You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1016 lines
26 KiB
JavaScript
1016 lines
26 KiB
JavaScript
var clippy = {};
|
|
|
|
/******
|
|
*
|
|
*
|
|
* @constructor
|
|
*/
|
|
clippy.Agent = function (path, data, sounds) {
|
|
this.path = path;
|
|
|
|
this._queue = new clippy.Queue($.proxy(this._onQueueEmpty, this));
|
|
|
|
this._el = $('<div class="clippy"></div>').hide();
|
|
|
|
$(document.body).append(this._el);
|
|
|
|
this._animator = new clippy.Animator(this._el, path, data, sounds);
|
|
|
|
this._balloon = new clippy.Balloon(this._el);
|
|
|
|
this._setupEvents();
|
|
};
|
|
|
|
clippy.Agent.prototype = {
|
|
|
|
/**************************** API ************************************/
|
|
|
|
/***
|
|
*
|
|
* @param {Number} x
|
|
* @param {Number} y
|
|
*/
|
|
gestureAt:function (x, y) {
|
|
var d = this._getDirection(x, y);
|
|
var gAnim = 'Gesture' + d;
|
|
var lookAnim = 'Look' + d;
|
|
|
|
var animation = this.hasAnimation(gAnim) ? gAnim : lookAnim;
|
|
return this.play(animation);
|
|
},
|
|
|
|
/***
|
|
*
|
|
* @param {Boolean=} fast
|
|
*
|
|
*/
|
|
hide:function (fast, callback) {
|
|
this._hidden = true;
|
|
var el = this._el;
|
|
this.stop();
|
|
if (fast) {
|
|
this._el.hide();
|
|
this.stop();
|
|
this.pause();
|
|
if (callback) callback();
|
|
return;
|
|
}
|
|
|
|
return this._playInternal('Hide', function () {
|
|
el.hide();
|
|
this.pause();
|
|
if (callback) callback();
|
|
})
|
|
},
|
|
|
|
|
|
moveTo:function (x, y, duration) {
|
|
var dir = this._getDirection(x, y);
|
|
var anim = 'Move' + dir;
|
|
if (duration === undefined) duration = 1000;
|
|
|
|
this._addToQueue(function (complete) {
|
|
// the simple case
|
|
if (duration === 0) {
|
|
this._el.css({top:y, left:x});
|
|
this.reposition();
|
|
complete();
|
|
return;
|
|
}
|
|
|
|
// no animations
|
|
if (!this.hasAnimation(anim)) {
|
|
this._el.animate({top:y, left:x}, duration, complete);
|
|
return;
|
|
}
|
|
|
|
var callback = $.proxy(function (name, state) {
|
|
// when exited, complete
|
|
if (state === clippy.Animator.States.EXITED) {
|
|
complete();
|
|
}
|
|
// if waiting,
|
|
if (state === clippy.Animator.States.WAITING) {
|
|
this._el.animate({top:y, left:x}, duration, $.proxy(function () {
|
|
// after we're done with the movement, do the exit animation
|
|
this._animator.exitAnimation();
|
|
}, this));
|
|
}
|
|
|
|
}, this);
|
|
|
|
this._playInternal(anim, callback);
|
|
}, this);
|
|
},
|
|
|
|
_playInternal:function (animation, callback) {
|
|
|
|
// if we're inside an idle animation,
|
|
if (this._isIdleAnimation() && this._idleDfd && this._idleDfd.state() === 'pending') {
|
|
this._idleDfd.done($.proxy(function () {
|
|
this._playInternal(animation, callback);
|
|
}, this))
|
|
}
|
|
|
|
this._animator.showAnimation(animation, callback);
|
|
},
|
|
|
|
play:function (animation, timeout, cb) {
|
|
if (!this.hasAnimation(animation)) return false;
|
|
|
|
if (timeout === undefined) timeout = 5000;
|
|
|
|
|
|
this._addToQueue(function (complete) {
|
|
var completed = false;
|
|
// handle callback
|
|
var callback = function (name, state) {
|
|
if (state === clippy.Animator.States.EXITED) {
|
|
completed = true;
|
|
if (cb) cb();
|
|
complete();
|
|
}
|
|
};
|
|
|
|
// if has timeout, register a timeout function
|
|
if (timeout) {
|
|
window.setTimeout($.proxy(function () {
|
|
if (completed) return;
|
|
// exit after timeout
|
|
this._animator.exitAnimation();
|
|
}, this), timeout)
|
|
}
|
|
|
|
this._playInternal(animation, callback);
|
|
}, this);
|
|
|
|
return true;
|
|
},
|
|
|
|
/***
|
|
*
|
|
* @param {Boolean=} fast
|
|
*/
|
|
show:function (fast) {
|
|
|
|
this._hidden = false;
|
|
if (fast) {
|
|
this._el.show();
|
|
this.resume();
|
|
this._onQueueEmpty();
|
|
return;
|
|
}
|
|
|
|
if (this._el.css('top') === 'auto' || !this._el.css('left') === 'auto') {
|
|
var left = $(window).width() * 0.8;
|
|
var top = ($(window).height() + $(document).scrollTop()) * 0.8;
|
|
this._el.css({top:top, left:left});
|
|
}
|
|
|
|
this.resume();
|
|
return this.play('Show');
|
|
},
|
|
|
|
/***
|
|
*
|
|
* @param {String} text
|
|
*/
|
|
speak:function (text, hold) {
|
|
this._addToQueue(function (complete) {
|
|
this._balloon.speak(complete, text, hold);
|
|
}, this);
|
|
},
|
|
|
|
|
|
/***
|
|
* Close the current balloon
|
|
*/
|
|
closeBalloon:function () {
|
|
this._balloon.hide();
|
|
},
|
|
|
|
delay:function (time) {
|
|
time = time || 250;
|
|
|
|
this._addToQueue(function (complete) {
|
|
this._onQueueEmpty();
|
|
window.setTimeout(complete, time);
|
|
});
|
|
},
|
|
|
|
/***
|
|
* Skips the current animation
|
|
*/
|
|
stopCurrent:function () {
|
|
this._animator.exitAnimation();
|
|
this._balloon.close();
|
|
},
|
|
|
|
|
|
stop:function () {
|
|
// clear the queue
|
|
this._queue.clear();
|
|
this._animator.exitAnimation();
|
|
this._balloon.hide();
|
|
},
|
|
|
|
/***
|
|
*
|
|
* @param {String} name
|
|
* @returns {Boolean}
|
|
*/
|
|
hasAnimation:function (name) {
|
|
return this._animator.hasAnimation(name);
|
|
},
|
|
|
|
/***
|
|
* Gets a list of animation names
|
|
*
|
|
* @return {Array.<string>}
|
|
*/
|
|
animations:function () {
|
|
return this._animator.animations();
|
|
},
|
|
|
|
/***
|
|
* Play a random animation
|
|
* @return {jQuery.Deferred}
|
|
*/
|
|
animate:function () {
|
|
var animations = this.animations();
|
|
var anim = animations[Math.floor(Math.random() * animations.length)];
|
|
// skip idle animations
|
|
if (anim.indexOf('Idle') === 0) {
|
|
return this.animate();
|
|
}
|
|
return this.play(anim);
|
|
},
|
|
|
|
/**************************** Utils ************************************/
|
|
|
|
/***
|
|
*
|
|
* @param {Number} x
|
|
* @param {Number} y
|
|
* @return {String}
|
|
* @private
|
|
*/
|
|
_getDirection:function (x, y) {
|
|
var offset = this._el.offset();
|
|
var h = this._el.height();
|
|
var w = this._el.width();
|
|
|
|
var centerX = (offset.left + w / 2);
|
|
var centerY = (offset.top + h / 2);
|
|
|
|
|
|
var a = centerY - y;
|
|
var b = centerX - x;
|
|
|
|
var r = Math.round((180 * Math.atan2(a, b)) / Math.PI);
|
|
|
|
// Left and Right are for the character, not the screen :-/
|
|
if (-45 <= r && r < 45) return 'Right';
|
|
if (45 <= r && r < 135) return 'Up';
|
|
if (135 <= r && r <= 180 || -180 <= r && r < -135) return 'Left';
|
|
if (-135 <= r && r < -45) return 'Down';
|
|
|
|
// sanity check
|
|
return 'Top';
|
|
},
|
|
|
|
/**************************** Queue and Idle handling ************************************/
|
|
|
|
/***
|
|
* Handle empty queue.
|
|
* We need to transition the animation to an idle state
|
|
* @private
|
|
*/
|
|
_onQueueEmpty:function () {
|
|
if (this._hidden || this._isIdleAnimation()) return;
|
|
var idleAnim = this._getIdleAnimation();
|
|
this._idleDfd = $.Deferred();
|
|
|
|
this._animator.showAnimation(idleAnim, $.proxy(this._onIdleComplete, this));
|
|
},
|
|
|
|
_onIdleComplete:function (name, state) {
|
|
if (state === clippy.Animator.States.EXITED) {
|
|
this._idleDfd.resolve();
|
|
}
|
|
},
|
|
|
|
|
|
/***
|
|
* Is the current animation is Idle?
|
|
* @return {Boolean}
|
|
* @private
|
|
*/
|
|
_isIdleAnimation:function () {
|
|
var c = this._animator.currentAnimationName;
|
|
return c && c.indexOf('Idle') === 0;
|
|
},
|
|
|
|
|
|
/**
|
|
* Gets a random Idle animation
|
|
* @return {String}
|
|
* @private
|
|
*/
|
|
_getIdleAnimation:function () {
|
|
var animations = this.animations();
|
|
var r = [];
|
|
for (var i = 0; i < animations.length; i++) {
|
|
var a = animations[i];
|
|
if (a.indexOf('Idle') === 0) {
|
|
r.push(a);
|
|
}
|
|
}
|
|
|
|
// pick one
|
|
var idx = Math.floor(Math.random() * r.length);
|
|
return r[idx];
|
|
},
|
|
|
|
/**************************** Events ************************************/
|
|
|
|
_setupEvents:function () {
|
|
$(window).on('resize', $.proxy(this.reposition, this));
|
|
|
|
this._el.on('mousedown', $.proxy(this._onMouseDown, this));
|
|
|
|
this._el.on('dblclick', $.proxy(this._onDoubleClick, this));
|
|
},
|
|
|
|
_onDoubleClick:function () {
|
|
if (!this.play('ClickedOn')) {
|
|
this.animate();
|
|
}
|
|
},
|
|
|
|
reposition:function () {
|
|
if (!this._el.is(':visible')) return;
|
|
var o = this._el.offset();
|
|
var bH = this._el.outerHeight();
|
|
var bW = this._el.outerWidth();
|
|
|
|
var wW = $(window).width();
|
|
var wH = $(window).height();
|
|
var sT = $(window).scrollTop();
|
|
var sL = $(window).scrollLeft();
|
|
|
|
var top = o.top - sT;
|
|
var left = o.left - sL;
|
|
var m = 5;
|
|
if (top - m < 0) {
|
|
top = m;
|
|
} else if ((top + bH + m) > wH) {
|
|
top = wH - bH - m;
|
|
}
|
|
|
|
if (left - m < 0) {
|
|
left = m;
|
|
} else if (left + bW + m > wW) {
|
|
left = wW - bW - m;
|
|
}
|
|
|
|
this._el.css({left:left, top:top});
|
|
// reposition balloon
|
|
this._balloon.reposition();
|
|
},
|
|
|
|
_onMouseDown:function (e) {
|
|
e.preventDefault();
|
|
this._startDrag(e);
|
|
},
|
|
|
|
|
|
/**************************** Drag ************************************/
|
|
|
|
_startDrag:function (e) {
|
|
// pause animations
|
|
this.pause();
|
|
this._balloon.hide(true);
|
|
this._offset = this._calculateClickOffset(e);
|
|
|
|
this._moveHandle = $.proxy(this._dragMove, this);
|
|
this._upHandle = $.proxy(this._finishDrag, this);
|
|
|
|
$(window).on('mousemove', this._moveHandle);
|
|
$(window).on('mouseup', this._upHandle);
|
|
|
|
this._dragUpdateLoop = window.setTimeout($.proxy(this._updateLocation, this), 10);
|
|
},
|
|
|
|
_calculateClickOffset:function (e) {
|
|
var mouseX = e.pageX;
|
|
var mouseY = e.pageY;
|
|
var o = this._el.offset();
|
|
return {
|
|
top:mouseY - o.top,
|
|
left:mouseX - o.left
|
|
}
|
|
|
|
},
|
|
|
|
_updateLocation:function () {
|
|
this._el.css({top:this._targetY, left:this._taregtX});
|
|
this._dragUpdateLoop = window.setTimeout($.proxy(this._updateLocation, this), 10);
|
|
},
|
|
|
|
_dragMove:function (e) {
|
|
e.preventDefault();
|
|
var x = e.clientX - this._offset.left;
|
|
var y = e.clientY - this._offset.top;
|
|
this._taregtX = x;
|
|
this._targetY = y;
|
|
},
|
|
|
|
_finishDrag:function () {
|
|
window.clearTimeout(this._dragUpdateLoop);
|
|
// remove handles
|
|
$(window).off('mousemove', this._moveHandle);
|
|
$(window).off('mouseup', this._upHandle);
|
|
// resume animations
|
|
this._balloon.show();
|
|
this.reposition();
|
|
this.resume();
|
|
|
|
},
|
|
|
|
_addToQueue:function (func, scope) {
|
|
if (scope) func = $.proxy(func, scope);
|
|
this._queue.queue(func);
|
|
},
|
|
|
|
/**************************** Pause and Resume ************************************/
|
|
|
|
pause:function () {
|
|
this._animator.pause();
|
|
this._balloon.pause();
|
|
|
|
},
|
|
|
|
resume:function () {
|
|
this._animator.resume();
|
|
this._balloon.resume();
|
|
}
|
|
|
|
};
|
|
|
|
/******
|
|
*
|
|
*
|
|
* @constructor
|
|
*/
|
|
clippy.Animator = function (el, path, data, sounds) {
|
|
this._el = el;
|
|
this._data = data;
|
|
this._path = path;
|
|
this._currentFrameIndex = 0;
|
|
this._currentFrame = undefined;
|
|
this._exiting = false;
|
|
this._currentAnimation = undefined;
|
|
this._endCallback = undefined;
|
|
this._started = false;
|
|
this._sounds = {};
|
|
this.currentAnimationName = undefined;
|
|
this.preloadSounds(sounds);
|
|
this._overlays = [this._el];
|
|
var curr = this._el;
|
|
|
|
this._setupElement(this._el);
|
|
for (var i = 1; i < this._data.overlayCount; i++) {
|
|
var inner = this._setupElement($('<div></div>'));
|
|
|
|
curr.append(inner);
|
|
this._overlays.push(inner);
|
|
curr = inner;
|
|
}
|
|
};
|
|
|
|
clippy.Animator.prototype = {
|
|
_setupElement:function (el) {
|
|
var frameSize = this._data.framesize;
|
|
el.css('display', "none");
|
|
el.css({width:frameSize[0], height:frameSize[1]});
|
|
el.css('background', "url('" + this._path + "/map.png') no-repeat");
|
|
|
|
return el;
|
|
},
|
|
|
|
animations:function () {
|
|
var r = [];
|
|
var d = this._data.animations;
|
|
for (var n in d) {
|
|
r.push(n);
|
|
}
|
|
return r;
|
|
},
|
|
|
|
preloadSounds:function (sounds) {
|
|
|
|
for (var i = 0; i < this._data.sounds.length; i++) {
|
|
var snd = this._data.sounds[i];
|
|
var uri = sounds[snd];
|
|
if (!uri) continue;
|
|
this._sounds[snd] = new Audio(uri);
|
|
|
|
}
|
|
},
|
|
hasAnimation:function (name) {
|
|
return !!this._data.animations[name];
|
|
},
|
|
|
|
exitAnimation:function () {
|
|
this._exiting = true;
|
|
},
|
|
|
|
|
|
showAnimation:function (animationName, stateChangeCallback) {
|
|
this._exiting = false;
|
|
|
|
if (!this.hasAnimation(animationName)) {
|
|
return false;
|
|
}
|
|
|
|
this._currentAnimation = this._data.animations[animationName];
|
|
this.currentAnimationName = animationName;
|
|
|
|
|
|
if (!this._started) {
|
|
this._step();
|
|
this._started = true;
|
|
}
|
|
|
|
this._currentFrameIndex = 0;
|
|
this._currentFrame = undefined;
|
|
this._endCallback = stateChangeCallback;
|
|
|
|
return true;
|
|
},
|
|
|
|
|
|
_draw:function () {
|
|
var images = [];
|
|
if (this._currentFrame) images = this._currentFrame.images || [];
|
|
|
|
for (var i = 0; i < this._overlays.length; i++) {
|
|
if (i < images.length) {
|
|
var xy = images[i];
|
|
var bg = -xy[0] + 'px ' + -xy[1] + 'px';
|
|
this._overlays[i].css({'background-position':bg, 'display':'block'});
|
|
}
|
|
else {
|
|
this._overlays[i].css('display', 'none');
|
|
}
|
|
|
|
}
|
|
},
|
|
|
|
_getNextAnimationFrame:function () {
|
|
if (!this._currentAnimation) return undefined;
|
|
// No current frame. start animation.
|
|
if (!this._currentFrame) return 0;
|
|
var currentFrame = this._currentFrame;
|
|
var branching = this._currentFrame.branching;
|
|
|
|
|
|
if (this._exiting && currentFrame.exitBranch !== undefined) {
|
|
return currentFrame.exitBranch;
|
|
}
|
|
else if (branching) {
|
|
var rnd = Math.random() * 100;
|
|
for (var i = 0; i < branching.branches.length; i++) {
|
|
var branch = branching.branches[i];
|
|
if (rnd <= branch.weight) {
|
|
return branch.frameIndex;
|
|
}
|
|
|
|
rnd -= branch.weight;
|
|
}
|
|
}
|
|
|
|
return this._currentFrameIndex + 1;
|
|
},
|
|
|
|
_playSound:function () {
|
|
var s = this._currentFrame.sound;
|
|
if (!s) return;
|
|
var audio = this._sounds[s];
|
|
if (audio) audio.play();
|
|
},
|
|
|
|
_atLastFrame:function () {
|
|
return this._currentFrameIndex >= this._currentAnimation.frames.length - 1;
|
|
},
|
|
|
|
_step:function () {
|
|
if (!this._currentAnimation) return;
|
|
var newFrameIndex = Math.min(this._getNextAnimationFrame(), this._currentAnimation.frames.length - 1);
|
|
var frameChanged = !this._currentFrame || this._currentFrameIndex !== newFrameIndex;
|
|
this._currentFrameIndex = newFrameIndex;
|
|
|
|
// always switch frame data, unless we're at the last frame of an animation with a useExitBranching flag.
|
|
if (!(this._atLastFrame() && this._currentAnimation.useExitBranching)) {
|
|
this._currentFrame = this._currentAnimation.frames[this._currentFrameIndex];
|
|
}
|
|
|
|
this._draw();
|
|
this._playSound();
|
|
|
|
this._loop = window.setTimeout($.proxy(this._step, this), this._currentFrame.duration);
|
|
|
|
|
|
// fire events if the frames changed and we reached an end
|
|
if (this._endCallback && frameChanged && this._atLastFrame()) {
|
|
if (this._currentAnimation.useExitBranching && !this._exiting) {
|
|
this._endCallback(this.currentAnimationName, clippy.Animator.States.WAITING);
|
|
}
|
|
else {
|
|
this._endCallback(this.currentAnimationName, clippy.Animator.States.EXITED);
|
|
}
|
|
}
|
|
},
|
|
|
|
/***
|
|
* Pause animation execution
|
|
*/
|
|
pause:function () {
|
|
window.clearTimeout(this._loop);
|
|
},
|
|
|
|
/***
|
|
* Resume animation
|
|
*/
|
|
resume:function () {
|
|
this._step();
|
|
}
|
|
};
|
|
|
|
clippy.Animator.States = { WAITING:1, EXITED:0 };
|
|
|
|
/******
|
|
*
|
|
*
|
|
* @constructor
|
|
*/
|
|
clippy.Balloon = function (targetEl) {
|
|
this._targetEl = targetEl;
|
|
|
|
this._hidden = true;
|
|
this._setup();
|
|
};
|
|
|
|
clippy.Balloon.prototype = {
|
|
|
|
WORD_SPEAK_TIME:320,
|
|
CLOSE_BALLOON_DELAY:2000,
|
|
|
|
_setup:function () {
|
|
|
|
this._balloon = $('<div class="clippy-balloon"><div class="clippy-tip"></div><div class="clippy-content"></div></div> ').hide();
|
|
this._content = this._balloon.find('.clippy-content');
|
|
|
|
$(document.body).append(this._balloon);
|
|
},
|
|
|
|
reposition:function () {
|
|
var sides = ['top-left', 'top-right', 'bottom-left', 'bottom-right'];
|
|
|
|
for (var i = 0; i < sides.length; i++) {
|
|
var s = sides[i];
|
|
this._position(s);
|
|
if (!this._isOut()) break;
|
|
}
|
|
},
|
|
|
|
_BALLOON_MARGIN:15,
|
|
|
|
/***
|
|
*
|
|
* @param side
|
|
* @private
|
|
*/
|
|
_position:function (side) {
|
|
var o = this._targetEl.offset();
|
|
var h = this._targetEl.height();
|
|
var w = this._targetEl.width();
|
|
|
|
var bH = this._balloon.outerHeight();
|
|
var bW = this._balloon.outerWidth();
|
|
|
|
this._balloon.removeClass('clippy-top-left');
|
|
this._balloon.removeClass('clippy-top-right');
|
|
this._balloon.removeClass('clippy-bottom-right');
|
|
this._balloon.removeClass('clippy-bottom-left');
|
|
|
|
var left, top;
|
|
switch (side) {
|
|
case 'top-left':
|
|
// right side of the balloon next to the right side of the agent
|
|
left = o.left + w - bW;
|
|
top = o.top - bH - this._BALLOON_MARGIN;
|
|
break;
|
|
case 'top-right':
|
|
// left side of the balloon next to the left side of the agent
|
|
left = o.left;
|
|
top = o.top - bH - this._BALLOON_MARGIN;
|
|
break;
|
|
case 'bottom-right':
|
|
// right side of the balloon next to the right side of the agent
|
|
left = o.left;
|
|
top = o.top + h + this._BALLOON_MARGIN;
|
|
break;
|
|
case 'bottom-left':
|
|
// left side of the balloon next to the left side of the agent
|
|
left = o.left + w - bW;
|
|
top = o.top + h + this._BALLOON_MARGIN;
|
|
break;
|
|
}
|
|
|
|
this._balloon.css({top:top, left:left});
|
|
this._balloon.addClass('clippy-' + side);
|
|
},
|
|
|
|
_isOut:function () {
|
|
var o = this._balloon.offset();
|
|
var bH = this._balloon.outerHeight();
|
|
var bW = this._balloon.outerWidth();
|
|
|
|
var wW = $(window).width();
|
|
var wH = $(window).height();
|
|
var sT = $(document).scrollTop();
|
|
var sL = $(document).scrollLeft();
|
|
|
|
var top = o.top - sT;
|
|
var left = o.left - sL;
|
|
var m = 5;
|
|
if (top - m < 0 || left - m < 0) return true;
|
|
if ((top + bH + m) > wH || (left + bW + m) > wW) return true;
|
|
|
|
return false;
|
|
},
|
|
|
|
speak:function (complete, text, hold) {
|
|
this._hidden = false;
|
|
this.show();
|
|
var c = this._content;
|
|
// set height to auto
|
|
c.height('auto');
|
|
c.width('auto');
|
|
// add the text
|
|
c.text(text);
|
|
// set height
|
|
c.height(c.height());
|
|
c.width(c.width());
|
|
c.text('');
|
|
this.reposition();
|
|
|
|
this._complete = complete;
|
|
this._sayWords(text, hold, complete);
|
|
},
|
|
|
|
show:function () {
|
|
if (this._hidden) return;
|
|
this._balloon.show();
|
|
},
|
|
|
|
hide:function (fast) {
|
|
if (fast) {
|
|
this._balloon.hide();
|
|
return;
|
|
}
|
|
|
|
this._hiding = window.setTimeout($.proxy(this._finishHideBalloon, this), this.CLOSE_BALLOON_DELAY);
|
|
},
|
|
|
|
_finishHideBalloon:function () {
|
|
if (this._active) return;
|
|
this._balloon.hide();
|
|
this._hidden = true;
|
|
this._hiding = null;
|
|
},
|
|
|
|
_sayWords:function (text, hold, complete) {
|
|
this._active = true;
|
|
this._hold = hold;
|
|
var words = text.split(/[^\S-]/);
|
|
var time = this.WORD_SPEAK_TIME;
|
|
var el = this._content;
|
|
var idx = 1;
|
|
|
|
|
|
this._addWord = $.proxy(function () {
|
|
if (!this._active) return;
|
|
if (idx > words.length) {
|
|
this._active = false;
|
|
if (!this._hold) {
|
|
complete();
|
|
this.hide();
|
|
}
|
|
} else {
|
|
el.text(words.slice(0, idx).join(' '));
|
|
idx++;
|
|
this._loop = window.setTimeout($.proxy(this._addWord, this), time);
|
|
}
|
|
}, this);
|
|
|
|
this._addWord();
|
|
|
|
},
|
|
|
|
close:function () {
|
|
if (this._active) {
|
|
this._hold = false;
|
|
} else if (this._hold) {
|
|
this._complete();
|
|
}
|
|
},
|
|
|
|
pause:function () {
|
|
window.clearTimeout(this._loop);
|
|
if (this._hiding) {
|
|
window.clearTimeout(this._hiding);
|
|
this._hiding = null;
|
|
}
|
|
},
|
|
|
|
resume:function () {
|
|
if (this._addWord) this._addWord();
|
|
this._hiding = window.setTimeout($.proxy(this._finishHideBalloon, this), this.CLOSE_BALLOON_DELAY);
|
|
}
|
|
|
|
|
|
};
|
|
|
|
clippy.BASE_PATH = './clippy.js/Agents/';
|
|
|
|
clippy.load = function (name, successCb, failCb) {
|
|
var path = clippy.BASE_PATH + name;
|
|
|
|
var mapDfd = clippy.load._loadMap(path);
|
|
var agentDfd = clippy.load._loadAgent(name, path);
|
|
var soundsDfd = clippy.load._loadSounds(name, path);
|
|
|
|
var data;
|
|
agentDfd.done(function (d) {
|
|
data = d;
|
|
});
|
|
|
|
var sounds;
|
|
|
|
soundsDfd.done(function (d) {
|
|
sounds = d;
|
|
});
|
|
|
|
// wrapper to the success callback
|
|
var cb = function () {
|
|
var a = new clippy.Agent(path, data,sounds);
|
|
successCb(a);
|
|
};
|
|
|
|
$.when(mapDfd, agentDfd, soundsDfd).done(cb).fail(failCb);
|
|
};
|
|
|
|
clippy.load._maps = {};
|
|
clippy.load._loadMap = function (path) {
|
|
var dfd = clippy.load._maps[path];
|
|
if (dfd) return dfd;
|
|
|
|
// set dfd if not defined
|
|
dfd = clippy.load._maps[path] = $.Deferred();
|
|
|
|
var src = path + '/map.png';
|
|
var img = new Image();
|
|
|
|
img.onload = dfd.resolve;
|
|
img.onerror = dfd.reject;
|
|
|
|
// start loading the map;
|
|
img.setAttribute('src', src);
|
|
|
|
return dfd.promise();
|
|
};
|
|
|
|
clippy.load._sounds = {};
|
|
|
|
clippy.load._loadSounds = function (name, path) {
|
|
var dfd = clippy.load._sounds[name];
|
|
if (dfd) return dfd;
|
|
|
|
// set dfd if not defined
|
|
dfd = clippy.load._sounds[name] = $.Deferred();
|
|
|
|
var audio = document.createElement('audio');
|
|
var canPlayMp3 = !!audio.canPlayType && "" != audio.canPlayType('audio/mpeg');
|
|
var canPlayOgg = !!audio.canPlayType && "" != audio.canPlayType('audio/ogg; codecs="vorbis"');
|
|
|
|
if (!canPlayMp3 && !canPlayOgg) {
|
|
dfd.resolve({});
|
|
} else {
|
|
var src = path + (canPlayMp3 ? '/sounds-mp3.js' : '/sounds-ogg.js');
|
|
// load
|
|
clippy.load._loadScript(src);
|
|
}
|
|
|
|
return dfd.promise()
|
|
};
|
|
|
|
|
|
clippy.load._data = {};
|
|
clippy.load._loadAgent = function (name, path) {
|
|
var dfd = clippy.load._data[name];
|
|
if (dfd) return dfd;
|
|
|
|
dfd = clippy.load._getAgentDfd(name);
|
|
|
|
var src = path + '/agent.js';
|
|
|
|
clippy.load._loadScript(src);
|
|
|
|
return dfd.promise();
|
|
};
|
|
|
|
clippy.load._loadScript = function (src) {
|
|
var script = document.createElement('script');
|
|
script.setAttribute('src', src);
|
|
script.setAttribute('async', 'async');
|
|
script.setAttribute('type', 'text/javascript');
|
|
|
|
document.head.appendChild(script);
|
|
};
|
|
|
|
clippy.load._getAgentDfd = function (name) {
|
|
var dfd = clippy.load._data[name];
|
|
if (!dfd) {
|
|
dfd = clippy.load._data[name] = $.Deferred();
|
|
}
|
|
return dfd;
|
|
};
|
|
|
|
clippy.ready = function (name, data) {
|
|
var dfd = clippy.load._getAgentDfd(name);
|
|
dfd.resolve(data);
|
|
};
|
|
|
|
clippy.soundsReady = function (name, data) {
|
|
var dfd = clippy.load._sounds[name];
|
|
if (!dfd) {
|
|
dfd = clippy.load._sounds[name] = $.Deferred();
|
|
}
|
|
|
|
dfd.resolve(data);
|
|
};
|
|
|
|
/******
|
|
* Tiny Queue
|
|
*
|
|
* @constructor
|
|
*/
|
|
clippy.Queue = function (onEmptyCallback) {
|
|
this._queue = [];
|
|
this._onEmptyCallback = onEmptyCallback;
|
|
};
|
|
|
|
clippy.Queue.prototype = {
|
|
/***
|
|
*
|
|
* @param {function(Function)} func
|
|
* @returns {jQuery.Deferred}
|
|
*/
|
|
queue:function (func) {
|
|
this._queue.push(func);
|
|
|
|
if (this._queue.length === 1 && !this._active) {
|
|
this._progressQueue();
|
|
}
|
|
},
|
|
|
|
_progressQueue:function () {
|
|
|
|
// stop if nothing left in queue
|
|
if (!this._queue.length) {
|
|
this._onEmptyCallback();
|
|
return;
|
|
}
|
|
|
|
var f = this._queue.shift();
|
|
this._active = true;
|
|
|
|
// execute function
|
|
var completeFunction = $.proxy(this.next, this);
|
|
f(completeFunction);
|
|
},
|
|
|
|
clear:function () {
|
|
this._queue = [];
|
|
},
|
|
|
|
next:function () {
|
|
this._active = false;
|
|
this._progressQueue();
|
|
}
|
|
};
|
|
|