/* * Copyright (c) 2011-2013 Mathias Panzenböck * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ "use strict"; if (typeof(BrowserPonies) !== "object") { // Shims: (function () { var shim = function (obj, shims) { for (var name in shims) { if (!(name in obj)) { obj[name] = shims[name]; } } }; shim(String.prototype, { trim: function () { return this.replace(/^\s\s*/, '').replace(/\s\s*$/, ''); }, trimLeft: function () { return this.replace(/^\s\s*/, ''); }, trimRight: function () { return this.replace(/\s\s*$/, ''); } }); shim(Array, { isArray: function (object) { return Object.prototype.toString.call(object) === '[object Array]'; } }); shim(Array.prototype, { indexOf: function (searchElement, fromIndex) { if (!fromIndex || fromIndex < 0) fromIndex = 0; for (; fromIndex < this.length; ++ fromIndex) { if (this[fromIndex] === searchElement) { return fromIndex; } } return -1; } }); shim(Function.prototype, { bind: function (self) { var funct = this; var partial = Array.prototype.slice.call(arguments,1); return function () { return funct.apply(self,partial.concat(Array.prototype.slice.call(arguments))); }; } }); shim(Date, { now: function () { return new Date().getTime(); } }); // dummy console object to prevent crashes on forgotten debug messages: if (typeof(console) === "undefined") shim(window, {console: {}}); shim(window.console, {log: function () {}}); shim(window.console, { info: window.console.log, warn: window.console.log, error: window.console.log, trace: window.console.log, dir: window.console.log }); })(); var BrowserPonies = (function () { var BaseZIndex = 9000000; var observe = document.addEventListener ? function (element, event, handler) { element.addEventListener(event, handler, false); } : function (element, event, handler) { var wrapper = '_eventHandlingWrapper' in handler ? handler._eventHandlingWrapper : (handler._eventHandlingWrapper = function () { var event = window.event; if (!('stopPropagation' in event)) { event.stopPropagation = function () { this.cancelBubble = true; }; } if (!('preventDefault' in event)) { event.preventDefault = function() { this.returnValue = false; }; } if (!('target' in event)) { event.target = event.srcElement; } return handler.call(this,event); }); element.attachEvent('on'+event, wrapper); }; var stopObserving = document.removeEventListener ? function (element, event, handler) { element.removeEventListener(event, handler, false); } : function (element, event, handler) { if ('_eventHandlingWrapper' in handler) { element.detachEvent('on'+event, handler._eventHandlingWrapper); } }; var documentHidden = function () { var names = ['hidden', 'webkitHidden', 'mozHidden', 'msHidden']; for (var i = 0; i < names.length; ++ i) { var name = names[i]; if (name in document) { return document[name]; } } return false; }; var visibilitychange = function (event) { if (timer !== null) { if (documentHidden()) { clearTimeout(timer); } else { lastTime = Date.now(); tick(); } } }; if (typeof(document.hidden) !== 'undefined') { observe(document, 'visibilitychange', visibilitychange); } else if (typeof(document.webkitHidden) !== 'undefined') { observe(document, 'webkitvisibilitychange', visibilitychange); } else if (typeof(document.mozHidden) !== 'undefined') { observe(document, 'mozvisibilitychange', visibilitychange); } else if (typeof(document.msHidden) !== 'undefined') { observe(document, 'msvisibilitychange', visibilitychange); } var windowSize = 'innerWidth' in window ? function () { return { width: window.innerWidth, height: window.innerHeight }; } : function () { return { width: document.documentElement.clientWidth, height: document.documentElement.clientHeight }; }; var padd = function (s,fill,padding,right) { if (s.length >= fill) { return s; } padding = new Array(fill-s.length+1).join(padding); return right ? (padding + s) : (s + padding); }; var format = function (fmt) { var s = ''; var argind = 1; while (fmt) { var m = /^([^%]*)%(-)?(0)?(\d+)?(?:\.(\d+)?)?([dfesj%])(.*)$/.exec(fmt); if (!m) { s += fmt; break; } s += m[1]; fmt = m[7]; var right = m[2] !== '-'; var fill = m[4] ? parseInt(m[4]) : 0; var decimal = m[5] ? parseInt(m[5]) : 6; var padding = right ? (m[3] || ' ') : ' '; switch (m[6]) { case 'd': s += padd(parseInt(arguments[argind++]).toFixed(0),fill,padding,right); break; case 'f': s += padd(Number(arguments[argind++]).toFixed(decimal),fill,padding,right); break; case 'e': s += padd(Number(arguments[argind++]).toExponential(decimal),fill,padding,right); break; case 's': s += padd(String(arguments[argind++]),fill,' ',right); break; case 'j': s += padd(JSON.stringify(arguments[argind++]),fill,' ',right); break; case '%': s += padd('%',fill,' ',right); } } return s; }; var extend = function (dest, src) { for (var name in src) { dest[name] = src[name]; } return dest; }; var partial = function (fn) { var args = Array.prototype.slice.call(arguments,1); return function () { return fn.apply(this,args.concat(Array.prototype.slice.call(arguments))); }; }; var URL = function URL (url) { var absurl = URL.abs(url); var match = URL.FILE_REGEX.exec(absurl); if (!match) match = URL.NET_REGEX.exec(absurl); if (!match) { throw new URIError("Illegal URL: "+url); } this.protocol = match[1].toLowerCase(); this.username = match[2]; this.password = match[3]; this.hostname = match[4]; this.port = match[5]; this.pathname = match[6] || "/"; this.search = match[7] || ""; this.hash = match[8] || ""; if (!this.port) { this.port = URL.DEFAULT_PORTS[this.protocol]; } if (this.port && URL.DEFAULT_PORTS[this.protocol] !== this.port) { this.host = this.hostname+':'+this.port; } else { this.host = this.hostname; } }; URL.prototype = { toString: function () { return this.protocol+'//'+ (this.username || this.password ? (this.username || 'anonymous')+(this.password ? ':'+this.password : '')+'@' : '')+ this.hostname+(this.port && R4.URL.DEFAULT_PORTS[this.protocol] !== this.port ? ':'+this.port : '')+ this.pathname+this.search+this.hash; } }; extend(URL, { FILE_REGEX: /^(file:)\/\/()()()()([^#\?]*)(\?[^#]*)?(#.*)?$/i, NET_REGEX: /^([a-z][-_a-z0-9]*:)\/\/(?:([^:@\/]*)(?::([^:@\/]*))?@)?([^:@\/]*)(?::(\d+))?(?:(\/[^#\?]*)(\?[^#]*)?(#.*)?)?$/i, DEFAULT_PORTS: { "http:": "80", "https:": "443", "ftp:": "21", "ftps:": "990", "file:": "" }, abs: function (url, baseurl) { if (!baseurl) baseurl = window.location; if (url.slice(0,2) === '//') { return baseurl.protocol+url; } else if (url[0] === '/') { return baseurl.protocol+'//'+baseurl.host+url; } else if (url[0] === '#') { return baseurl.protocol+'//'+baseurl.host+baseurl.pathname+baseurl.search+url; } else if (url[0] === '?') { return baseurl.protocol+'//'+baseurl.host+baseurl.pathname+url; } else if ((/^[a-z][-_a-z0-9]*:/i).test(url)) { return url; } else { var path = baseurl.pathname.split('/'); path.pop(); if (path.length === 0) { path.push(""); } path.push(url); return baseurl.protocol+'//'+baseurl.host+path.join("/"); } }, join: function (baseurl) { for (var i = 0; i < arguments.length; ++ i) { var url = arguments[i]; if ((/^[a-z][-_a-z0-9]*:/i).test(url)) { baseurl = url; } else { baseurl = new URL(baseurl); if (url.slice(0,2) === '//') { baseurl = baseurl.protocol+url; } else if (url[0] === '/') { baseurl = baseurl.protocol+'//'+baseurl.host+url; } else if (url[0] === '#') { baseurl = baseurl.protocol+'//'+baseurl.host+baseurl.pathname+baseurl.search+url; } else if (url[0] === '?') { baseurl = baseurl.protocol+'//'+baseurl.host+baseurl.pathname+url; } else { baseurl = baseurl.protocol+'//'+baseurl.host+baseurl.pathname+url; } } } return URL.fix(baseurl); }, fix: function (url) { return url.replace(/^https?:\/\/web\d?\.student\.tuwien\.ac\.at\/~e0427417\/browser-ponies\//,"https://panzi.github.com/Browser-Ponies/"); } }); var Opera = Object.prototype.toString.call(window.opera) === '[object Opera]'; var IE, IEVersion; (function () { var m = (/MSIE ([0-9]{1,}[\.0-9]{0,})/).exec(navigator.userAgent); IE = !!m; if (IE) { IEVersion = m[1].split("."); for (var i = 0; i < IEVersion.length; ++ i) { IEVersion[i] = parseInt(IEVersion[i], 10); } } })(); var Gecko = navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') === -1; var HasAudio = typeof(Audio) !== "undefined"; var add = function (element, arg) { if (!arg) return; if (typeof(arg) === "string") { element.appendChild(document.createTextNode(arg)); } else if (Array.isArray(arg)) { for (var i = 0, n = arg.length; i < n; ++ i) { add(element, arg[i]); } } else if (arg.nodeType === 1 || arg.nodeType === 3) { element.appendChild(arg); } else { for (var attr in arg) { var value = arg[attr]; if (attr === "class" || attr === "className") { element.className = String(value); } else if (attr === "for" || attr === "htmlFor") { element.htmlFor = String(value); } else if (/^on/.test(attr)) { if (typeof(value) !== "function") { throw new Error("Event listeners must be a function."); } observe(element, attr.replace(/^on/,""), value); } else if (attr === 'style') { if (typeof(value) === "object") { for (var name in value) { var cssValue = value[name]; if (name === 'float') { element.style.cssFloat = cssValue; element.style.styleFloat = cssValue; } else if (name === 'opacity') { setOpacity(element, Number(cssValue)); } else { try { element.style[name] = cssValue; } catch (e) { console.error(name+'='+cssValue+' '+e.toString()); } } } } else { element.style.cssText += ";"+value; } } else if (attr === 'value' && element.nodeName === 'TEXTAREA') { element.value = value; } else if (value === true) { element.setAttribute(attr,attr); } else if (value === false) { element.removeAttribute(attr); } else { element.setAttribute(attr,String(value)); } } } }; var setOpacity = IE && IEVersion[0] < 10 ? function (element, opacity) { try { element.style.filter = element.style.filter.replace(/\balpha\([^\)]*\)/gi,'') + 'alpha(opacity='+(Number(opacity)*100)+')'; } catch (e) {} element.style.opacity = opacity; } : function (element, opacity) { element.style.opacity = opacity; }; var tag = function (name) { var element = document.createElement(name); for (var i = 1, n = arguments.length; i < n; ++ i) { add(element, arguments[i]); } return element; }; var has = function (obj, name) { return Object.prototype.hasOwnProperty.call(obj, name); }; var removeAll = function (array, item) { for (var i = 0; i < array.length;) { if (array[i] === item) { array.splice(i,1); } else { ++ i; } } }; var dataUrl = function (mimeType, data) { return 'data:'+mimeType+';base64,'+Base64.encode(data); }; var escapeXml = function (s) { return s.replace(/&/g, '&').replace( //g, '>').replace( /"/g, '"').replace(/'/g, '''); }; // inspired by: // http://farhadi.ir/posts/utf8-in-javascript-with-a-new-trick var Base64 = { encode: function (input) { return btoa(unescape(encodeURIComponent(input))); }, decode: function (input) { return decodeURIComponent(escape(atob(input))); } }; var PonyINI = { parse: function (text) { var lines = text.split(/\r?\n/); var rows = []; for (var i = 0, n = lines.length; i < n; ++ i) { var line = lines[i].trim(); if (line.length === 0 || line.charAt(0) === "'") continue; var row = []; line = this.parseLine(line,row); if (line.length !== 0) { console.error("trailing text:",line); } rows.push(row); } return rows; }, parseLine: function (line,row) { var pos; while ((line = line.trimLeft()).length > 0) { var ch = line.charAt(0); switch (ch) { case '"': line = line.slice(1); pos = line.search('"'); if (pos < 0) pos = line.length; row.push(line.slice(0,pos)); line = line.slice(pos); if (line.length > 0) { ch = line.charAt(0); if (ch === '"') { line = line.slice(1).trimLeft(); ch = line.charAt(0); } if (line.length > 0) { if (ch === ',') { line = line.slice(1); } else if (ch !== '}') { console.error("data after quoted string:",line); } } } else { console.error("unterminated quoted string"); } break; case ',': line = line.slice(1); row.push(""); break; case '{': var nested = []; row.push(nested); line = this.parseLine(line.slice(1),nested).trimLeft(); if (line.length > 0) { ch = line.charAt(0); if (ch !== '}') { console.error("data after list:",line); } else { line = line.slice(1).trimLeft(); ch = line.charAt(0); } if (ch === ',') { line = line.slice(1); } } else { console.error("unterminated list"); } break; case '}': case '\n': return line; default: pos = line.search(/[,}]/); if (pos < 0) pos = line.length; row.push(line.slice(0,pos).trim()); line = line.slice(pos); if (line.length > 0) { ch = line.charAt(0); if (ch === ',') { line = line.slice(1); } else if (ch !== '}') { console.error("syntax error:",line); } } } } return line; } }; var parseBoolean = function (value) { var s = value.trim().toLowerCase(); if (s === "true") return true; else if (s === "false") return false; else throw new Error("illegal boolean value: "+value); }; var parsePoint = function (value) { if (typeof(value) === "string") value = value.split(","); if (value.length !== 2 || !/^\s*-?\d+\s*$/.test(value[0]) || !/^\s*-?\d+\s*$/.test(value[1])) { throw new Error("illegal point value: "+value.join(",")); } return {x: parseInt(value[0],10), y: parseInt(value[1],10)}; }; var $ = function (element_or_id) { if (typeof(element_or_id) === "string") { return document.getElementById(element_or_id); } else if (element_or_id && element_or_id.nodeType === 1) { return element_or_id; } else { return null; } }; var distance = function (p1, p2) { var dx = p2.x - p1.x; var dy = p2.y - p1.y; return Math.sqrt(dx*dx + dy*dy); }; var randomSelect = function (list) { return list[Math.floor(list.length * Math.random())]; }; var Movements = { Left: 0, Right: 1, Up: 2, Down: 3, UpLeft: 4, UpRight: 5, DownLeft: 6, DownRight: 7 }; var movementName = function (mov) { for (var name in Movements) { if (Movements[name] === mov) { return name; } } return "Not a Movement"; }; var AllowedMoves = { None: 0, HorizontalOnly: 1, VerticalOnly: 2, HorizontalVertical: 3, DiagonalOnly: 4, DiagonalHorizontal: 5, DiagonalVertical: 6, All: 7, MouseOver: 8, Sleep: 9, Dragged: 10 }; var Locations = { Top: 0, Bottom: 1, Left: 2, Right: 3, BottomRight: 4, BottomLeft: 5, TopRight: 6, TopLeft: 7, Center: 8, Any: 9, AnyNotCenter: 10 }; var AudioMimeTypes = { wav: 'audio/wav', webm: 'audio/webm', mpeg: 'audio/mpeg', mpga: 'audio/mpeg', mpg: 'audio/mpeg', mp1: 'audio/mpeg;codecs="mp1"', mp2: 'audio/mpeg;codecs="mp2"', mp3: 'audio/mpeg;codecs="mp3"', mp4: 'audio/mp4', mp4a: 'audio/mp4', ogg: 'audio/ogg', oga: 'audio/ogg', flac: 'audio/ogg;codecs="flac"', spx: 'audio/ogg;codecs="speex"' }; var locationName = function (loc) { for (var name in Locations) { if (Locations[name] === loc) { return name; } } return "Not a Location"; }; var Interaction = function Interaction (interaction) { this.name = interaction.name; this.probability = interaction.probability; this.proximity = interaction.proximity === "default" ? 640 : interaction.proximity; this.activate = interaction.activate; this.delay = interaction.delay; this.targets = []; this.behaviors = []; for (var i = 0, n = interaction.behaviors.length; i < n; ++ i) { this.behaviors.push(interaction.behaviors[i].toLowerCase()); } for (var i = 0, n = interaction.targets.length; i < n; ++ i) { var name = interaction.targets[i].toLowerCase(); if (!has(ponies, name)) { console.warn("Interaction "+this.name+" of pony "+interaction.pony+ " references non-existing pony "+name); } else { var pony = ponies[name]; for (var j = 0; j < this.behaviors.length;) { var behavior = this.behaviors[j]; if (has(pony.behaviors_by_name, behavior)) { ++ j; } else { this.behaviors.splice(j, 1); } } this.targets.push(pony); } } }; Interaction.prototype = { reachableTargets: function (pos) { var targets = []; var n = this.targets.length; if (n === 0) return targets; for (var i = 0; i < n; ++ i) { var pony = this.targets[i]; var instance = null; var instance_dist = Infinity; for (var j = 0, m = pony.instances.length; j < m; ++ j) { var inst = pony.instances[j]; var dist = distance(pos, inst.position()); if (dist <= this.proximity && dist < instance_dist) { instance = inst; instance_dist = dist; } } if (instance) { targets.push([instance_dist,instance]); } else if (this.activate === "all") { return null; } } if (targets.length === 0) { return null; } if (this.activate === "one") { targets.sort(function (lhs,rhs) { return lhs[0] - rhs[0]; }); return [targets[0][1]]; } else { for (var i = 0; i < targets.length; ++ i) { targets[i] = targets[i][1]; } } return targets; } }; var Behavior = function Behavior (baseurl, behavior) { extend(this, behavior); if (!this.name || this.name.toLowerCase() === 'none') { throw new Error(baseurl+': illegal behavior name '+this.name); } if (this.follow) this.follow = this.follow.toLowerCase(); this.movement = null; var movement = behavior.movement.replace(/[-_\s]/g,'').toLowerCase(); for (var name in AllowedMoves) { if (name.toLowerCase() === movement) { this.movement = AllowedMoves[name]; break; } } if (this.movement === null) { throw new Error(baseurl+": illegal movement "+behavior.movement+" for behavior "+behavior.name); } this.rightsize = {width: 0, height: 0}; if (behavior.rightimage) { this.rightimage = URL.join(baseurl, behavior.rightimage); } this.leftsize = {width: 0, height: 0}; if (behavior.leftimage) { this.leftimage = URL.join(baseurl, behavior.leftimage); } // XXX: bugfix for ini files: interprete (0, 0) as missing if (!this.rightcenter || (this.rightcenter.x === 0 && this.rightcenter.y === 0)) { this.rightcenter = {x: 0, y: 0, missing: true}; } if (!this.leftcenter || (this.leftcenter.x === 0 && this.leftcenter.y === 0)) { this.leftcenter = {x: 0, y: 0, missing: true}; } this.effects = []; this.effects_by_name = {}; if ('effects' in behavior) { for (var i = 0, n = behavior.effects.length; i < n; ++ i) { var effect = new Effect(baseurl, behavior.effects[i]); this.effects_by_name[effect.name.toLowerCase()] = effect; this.effects.push(effect); } } }; Behavior.prototype = { deref: function (property, pony) { var name = this[property]; var lower_name = (name||'').toLowerCase(); if (name && lower_name !== 'none') { if (has(pony.behaviors_by_name, lower_name)) { this[property] = pony.behaviors_by_name[lower_name]; } else { console.warn(format("%s: Behavior %s of pony %s references non-existing behavior %s.", pony.baseurl, this.name, pony.name, name)); delete this[property]; } } else { delete this[property]; } }, preload: function () { for (var i = 0, n = this.effects.length; i < n; ++ i) { this.effects[i].preload(); } if (this.rightimage) { preloadImage(this.rightimage, function (image) { this.rightsize.width = image.width; this.rightsize.height = image.height; if (this.rightcenter.missing) { this.rightcenter = { x: Math.round(image.width * 0.5), y: Math.round(image.height * 0.5) }; } }.bind(this)); } if (this.leftimage) { preloadImage(this.leftimage, function (image) { this.leftsize.width = image.width; this.leftsize.height = image.height; if (this.leftcenter.missing) { this.leftcenter = { x: Math.round(image.width * 0.5), y: Math.round(image.height * 0.5) }; } }.bind(this)); } }, isMoving: function () { if (this.follow || this.x || this.x) return true; switch (this.movement) { case AllowedMoves.None: case AllowedMoves.MouseOver: case AllowedMoves.Sleep: return false; default: return true; } } }; var parseLocation = function (value) { var loc = value.replace(/[-_\s]/g,'').toLowerCase(); for (var name in Locations) { if (name.toLowerCase() === loc) { return Locations[name]; } } throw new Error('illegal location: '+value); }; var Effect = function Effect (baseurl, effect) { extend(this, effect); this.name = effect.name.toLowerCase(); var locs = ['rightloc','leftloc','rightcenter','leftcenter']; for (var i = 0; i < locs.length; ++ i) { var name = locs[i]; if (name in effect) { this[name] = parseLocation(effect[name]); } } this.rightsize = {width: 0, height: 0}; if (effect.rightimage) { this.rightimage = URL.join(baseurl, effect.rightimage); } this.rightcenter_point = {x: 0, y: 0}; this.leftsize = {width: 0, height: 0}; if (effect.leftimage) { this.leftimage = URL.join(baseurl, effect.leftimage); } this.leftcenter_point = {x: 0, y: 0}; }; Effect.prototype = { preload: function () { if (this.rightimage) { preloadImage(this.rightimage, function (image) { this.rightsize.width = image.width; this.rightsize.height = image.height; this.rightcenter_point = { x: Math.round(image.width * 0.5), y: Math.round(image.height * 0.5) }; }.bind(this)); } if (this.leftimage) { preloadImage(this.leftimage, function (image) { this.leftsize.width = image.width; this.leftsize.height = image.height; this.leftcenter_point = { x: Math.round(image.width * 0.5), y: Math.round(image.height * 0.5) }; }.bind(this)); } } }; var equalLength = function (s1, s2) { var n = Math.min(s1.length, s2.length); for (var i = 0; i < n; ++ i) { if (s1.charAt(i) !== s2.charAt(i)) { return i; } } return n; }; var resources = {}; var resource_count = 0; var resource_loaded_count = 0; var onload_callbacks = []; var onprogress_callbacks = []; var loadImage = function (loader,url,observer) { var image = loader.object = new Image(); observe(image, 'load', partial(observer,true)); observe(image, 'error', partial(observer,false)); observe(image, 'abort', partial(observer,false)); image.src = url; }; var createAudio = function (urls) { var audio = new Audio(); if (typeof(urls) === "string") { audio.src = urls; } else { for (var type in urls) { var source = tag('source', {src: urls[type]}); if (type !== "audio/x-unknown") source.type = type; audio.appendChild(source); } } return audio; }; var loadAudio = function (urls) { return function (loader,id,observer) { var audio = loader.object = createAudio(urls); observe(audio, 'loadeddata', partial(observer,true)); observe(audio, 'error', partial(observer,false)); observe(audio, 'abort', partial(observer,false)); audio.preload = 'auto'; }; }; var preloadImage = function (url,callback) { preload(loadImage,url,callback); }; var preloadAudio = function (urls,callback) { var fakeurl; if (typeof(urls) === "string") { fakeurl = urls; } else { var list = []; for (var type in urls) { list.push(urls[type]); } if (list.length === 0) { throw new Error("no audio url to preload"); } else if (list.length === 1) { fakeurl = list[0]; } else { var common = list[0]; for (var i = 1; i < list.length; ++ i) { var n = equalLength(common, list[i]); if (n !== common.length) { common = common.slice(0,n); } } for (var i = 0; i < list.length; ++ i) { list[i] = list[i].slice(common.length); } list.sort(); fakeurl = common+'{'+list.join('|')+'}'; } } preload(loadAudio(urls),fakeurl,callback); }; var preload = function (load,url,callback) { if (has(resources,url)) { if (callback) { var loader = resources[url]; if (loader.loaded) { callback(loader.object); } else { loader.callbacks.push(callback); } } } else { ++ resource_count; var loader = resources[url] = { loaded: false, callbacks: callback ? [callback] : [] }; load(loader, url, function (success) { if (loader.loaded) { console.error('resource loaded twice: '+url); return; } loader.loaded = true; ++ resource_loaded_count; if (success) { console.log(format('%3.0f%% %d of %d loaded: %s', resource_loaded_count * 100 / resource_count, resource_loaded_count, resource_count, url)); } else { console.error(format('%3.0f%% %d of %d load error: %s', resource_loaded_count * 100 / resource_count, resource_loaded_count, resource_count, url)); } for (var i = 0, n = onprogress_callbacks.length; i < n; ++ i) { onprogress_callbacks[i](resource_loaded_count, resource_count, url, success); } for (var i = 0, n = loader.callbacks.length; i < n; ++ i) { loader.callbacks[i](loader.object, success); } delete loader.callbacks; if (resource_loaded_count === resource_count) { for (var i = 0, n = onload_callbacks.length; i < n; ++ i) { onload_callbacks[i](); } onload_callbacks = []; } }); } }; preload(function (loader,url,observer) { if (document.body) { observer(true); } else { var loaded = false; var fireLoad = function () { if (!loaded) { loaded = true; observer(true); } }; if (document.addEventListener) { // all browsers but IE implement HTML5 DOMContentLoaded observe(document, 'DOMContentLoaded', fireLoad); } else { var checkReadyState = function () { if (document.readyState === 'complete') { stopObserving(document, 'readystatechange', checkReadyState); fireLoad(); } }; observe(document, 'readystatechange', checkReadyState); } // fallback observe(window, 'load', fireLoad); } }, document.location.href); var onload = function (callback) { if (resource_loaded_count === resource_count) { callback(); } else { onload_callbacks.push(callback); } }; var onprogress = function (callback) { onprogress_callbacks.push(callback); }; var resource_count_for_progress = 0; var progressbar = null; var insertProgressbar = function () { resource_count_for_progress = resource_loaded_count; document.body.appendChild(progressbar.container); centerProgressbar(); setTimeout(function () { if (progressbar && !progressbar.finished) { progressbar.container.style.display = ''; } }, 250); observe(window,'resize',centerProgressbar); stopObserving(window,'load',insertProgressbar); }; var centerProgressbar = function () { var winsize = windowSize(); var hide = false; if (progressbar.container.style.display === "none") { hide = true; progressbar.container.style.visibility = 'hidden'; progressbar.container.style.display = ''; } var width = progressbar.container.offsetWidth; var height = progressbar.container.offsetHeight; var labelHeight = progressbar.label.offsetHeight; if (hide) { progressbar.container.style.display = 'none'; progressbar.container.style.visibility = ''; } progressbar.container.style.left = Math.round((winsize.width - width) * 0.5)+'px'; progressbar.container.style.top = Math.round((winsize.height - height) * 0.5)+'px'; progressbar.label.style.top = Math.round((height - labelHeight) * 0.5)+'px'; }; onprogress(function (resource_loaded_count, resource_count, url) { if (showLoadProgress || progressbar) { if (!progressbar) { progressbar = { bar: tag('div', {style:{ margin: '0', padding: '0', borderStyle: 'none', width: '0', height: '100%', background: '#9BD6F4', MozBorderRadius: '5px', borderRadius: '5px' }}), label: tag('div', {style:{ position: 'absolute', margin: '0', padding: '0', borderStyle: 'none', top: '0px', left: '0px', width: '100%', textAlign: 'center' }}) }; progressbar.barcontainer = tag('div', {style:{ margin: '0', padding: '0', borderStyle: 'none', width: '100%', height: '100%', background: '#D8D8D8', MozBorderRadius: '5px', borderRadius: '5px' }}, progressbar.bar); progressbar.container = tag('div', {style:{ position: 'fixed', width: '450px', height: '30px', background: 'white', padding: '10px', margin: '0', MozBorderRadius: '5px', borderRadius: '5px', color: '#294256', fontWeight: 'bold', fontSize: '16px', opacity: '0.9', display: 'none', boxShadow: "2px 2px 12px rgba(0,0,0,0.4)", MozBoxShadow: "2px 2px 12px rgba(0,0,0,0.4)" }, onclick: function () { if (progressbar) { progressbar.container.style.display = 'none'; } }}, progressbar.barcontainer, progressbar.label); } if (progressbar.container.style.display === 'none') { resource_count_for_progress = resource_loaded_count; } progressbar.finished = resource_loaded_count === resource_count; var loaded = resource_loaded_count - resource_count_for_progress; var count = resource_count - resource_count_for_progress; var progress = count === 0 ? 1.0 : loaded / count; progressbar.bar.style.width = Math.round(progress * 450)+'px'; progressbar.label.innerHTML = format('Loading Ponies… %d%%',Math.floor(progress * 100)); if (!progressbar.container.parentNode) { if (document.body) { insertProgressbar(); } else { observe(window,'load',insertProgressbar); } } if (progressbar.finished) { setTimeout(function () { stopObserving(window,'resize',centerProgressbar); stopObserving(window,'load',insertProgressbar); if (progressbar && progressbar.container && progressbar.container.parentNode) { progressbar.container.parentNode.removeChild(progressbar.container); } progressbar = null; }, 500); } } }); var Pony = function Pony (pony) { this.baseurl = URL.join(globalBaseUrl, pony.baseurl||""); if (!pony.name) { throw new Error('pony with following base URL has no name: '+this.baseurl); } this.name = pony.name; this.behaviorgroups = pony.behaviorgroups || {}; this.all_behaviors = []; this.random_behaviors = []; this.mouseover_behaviors = []; this.dragged_behaviors = []; this.stand_behaviors = []; this.behaviors_by_name = {}; this.speeches = []; this.random_speeches = []; this.speeches_by_name = {}; this.interactions = []; this.instances = []; this.categories = []; if (pony.categories) { for (var i = 0, n = pony.categories.length; i < n; ++ i) { this.categories.push(pony.categories[i].toLowerCase()); } } if (pony.speeches) { for (var i = 0, n = pony.speeches.length; i < n; ++ i) { var speech = extend({},pony.speeches[i]); if (speech.files) { var count = 0; for (var type in speech.files) { speech.files[type] = URL.join(this.baseurl, speech.files[type]); ++ count; } if (count === 0) { delete speech.files; } } if (speech.name) { var lowername = speech.name.toLowerCase(); if (has(this.speeches_by_name,lowername)) { console.warn(format("%s: Speech name %s of pony %s is not unique.", this.baseurl, speech.name, pony.name)); } else { this.speeches_by_name[lowername] = speech; } } if (!('skip' in speech)) { speech.skip = false; } if (!speech.skip) { this.random_speeches.push(speech); } if ('group' in speech) { if (speech.group !== 0 && !has(this.behaviorgroups,speech.group)) { console.warn(format("%s: Speech %s references unknown behavior group %d.", this.baseurl, speech.name, speech.group)); } } else { speech.group = 0; } this.speeches.push(speech); } } var speakevents = ['speakstart','speakend']; if ('behaviors' in pony) { for (var i = 0, n = pony.behaviors.length; i < n; ++ i) { var behavior = new Behavior(this.baseurl, pony.behaviors[i]); var lowername = behavior.name.toLowerCase(); if (has(this.behaviors_by_name,lowername)) { console.warn(format("%s: Behavior name %s of pony %s is not unique.", this.baseurl, behavior.name, pony.name)); } else { // semantics like Dektop Ponies where the // first match is used for linked behaviors this.behaviors_by_name[lowername] = behavior; } for (var j = 0; j < speakevents.length; ++ j) { var speakevent = speakevents[j]; var speechname = behavior[speakevent]; if (speechname) { speechname = speechname.toLowerCase(); if (has(this.speeches_by_name,speechname)) { behavior[speakevent] = this.speeches_by_name[speechname]; } else { console.warn(format("%s: Behavior %s of pony %s references non-existing speech %s.", this.baseurl, behavior.name, pony.name, behavior[speakevent])); delete behavior[speakevent]; } } } this.all_behaviors.push(behavior); if (!('skip' in behavior)) { behavior.skip = false; } if (!behavior.skip) this.random_behaviors.push(behavior); switch (behavior.movement) { case AllowedMoves.MouseOver: this.mouseover_behaviors.push(behavior); if (!behavior.skip) this.stand_behaviors.push(behavior); break; case AllowedMoves.Dragged: this.dragged_behaviors.push(behavior); if (!behavior.skip) this.stand_behaviors.push(behavior); break; case AllowedMoves.None: if (!behavior.skip) this.stand_behaviors.push(behavior); break; } if ('group' in behavior) { if (behavior.group !== 0 && !has(this.behaviorgroups,behavior.group)) { console.warn(format("%s: Behavior %s references unknown behavior group %d.", this.baseurl, behavior.name, behavior.group)); } } else { behavior.group = 0; } } if (this.dragged_behaviors.length === 0 && this.mouseover_behaviors.length > 0) { this.dragged_behaviors = this.mouseover_behaviors.slice(); } if (this.stand_behaviors.length === 0) { for (var i = 0, n = this.all_behaviors.length; i < n; ++ i) { var behavior = this.all_behaviors[i]; if (behavior.movement === AllowedMoves.Sleep && !behavior.skip) { this.stand_behaviors.push(behavior); } } } if (this.stand_behaviors.length === 0) { console.warn(format("%s: Pony %s has no (non-skip) non-moving behavior.", this.baseurl, this.name)); } else if (this.mouseover_behaviors.length === 0) { this.mouseover_behaviors = this.stand_behaviors.slice(); } // dereference linked behaviors: for (var i = 0, n = this.all_behaviors.length; i < n; ++ i) { var behavior = this.all_behaviors[i]; behavior.deref('linked',this); behavior.deref('stopped',this); behavior.deref('moving',this); } } }; Pony.prototype = { preload: function () { for (var i = 0, n = this.all_behaviors.length; i < n; ++ i) { this.all_behaviors[i].preload(); } if (HasAudio && audioEnabled) { for (var i = 0, n = this.speeches.length; i < n; ++ i) { var speech = this.speeches[i]; if (speech.files) { preloadAudio(speech.files); } } } }, unspawnAll: function () { while (this.instances.length > 0) { this.instances[0].unspawn(); } }, addInteraction: function (interaction) { interaction = new Interaction(interaction); if (interaction.targets.length === 0) { console.warn("Dropping interaction "+interaction.name+" of pony "+this.name+ " because it has no targets."); return false; } for (var i = 0; i < interaction.behaviors.length;) { var behavior = interaction.behaviors[i]; if (has(this.behaviors_by_name, behavior)) { ++ i; } else { interaction.behaviors.splice(i, 1); } } if (interaction.behaviors.length === 0) { console.warn("Dropping interaction "+interaction.name+" of pony "+this.name+ " because it has no common behaviors."); return false; } this.interactions.push(interaction); return true; } }; var descendantOf = function (child, parent) { var node = child.parentNode; while (node) { if (node === parent) { return true; } } return false; }; var isOffscreen = function (rect) { return isOutsideOf(rect, windowSize()); }; // rect has origin at center // area is only a size var isOutsideOf = function (rect, area) { var wh = rect.width * 0.5; var hh = rect.height * 0.5; return rect.x < wh || rect.y < hh || rect.x + wh > area.width || rect.y + hh > area.height; }; var clipToScreen = function (rect) { var winsize = windowSize(); var x = rect.x; var y = rect.y; var wh = rect.width * 0.5; var hh = rect.height * 0.5; if (x < wh) { x = wh; } else if (x + wh > winsize.width) { x = winsize.width - wh; } if (y < hh) { y = hh; } else if (y + hh > winsize.height) { y = winsize.height - hh; } return {x: Math.round(x), y: Math.round(y)}; }; var Instance = function Instance () {}; Instance.prototype = { setTopLeftPosition: function (pos) { this.current_position.x = pos.x + this.current_center.x; this.current_position.y = pos.y + this.current_center.y; this.img.style.left = Math.round(pos.x)+'px'; this.img.style.top = Math.round(pos.y)+'px'; var zIndex = Math.round(BaseZIndex + pos.y + this.current_size.height); if (this.zIndex !== zIndex) { this.img.style.zIndex = zIndex; } }, setPosition: function (pos) { var x = this.current_position.x = pos.x; var y = this.current_position.y = pos.y; var top = Math.round(y - this.current_center.y); this.img.style.left = Math.round(x - this.current_center.x)+'px'; this.img.style.top = top+'px'; var zIndex = Math.round(BaseZIndex + top + this.current_size.height); if (this.zIndex !== zIndex) { this.img.style.zIndex = zIndex; } }, moveBy: function (offset) { this.setPosition({ x: this.current_position.x + offset.x, y: this.current_position.y + offset.y }); }, clipToScreen: function () { this.setPosition(clipToScreen(this.rect())); }, topLeftPosition: function () { return { x: this.current_position.x - this.current_center.x, y: this.current_position.y - this.current_center.y }; }, position: function () { return this.current_position; }, size: function () { return this.current_size; }, rect: function () { // lets abuse for speed (avoid object creation) var pos = this.current_position; pos.width = this.current_size.width; pos.height = this.current_size.height; return pos; // var pos = this.position(); // var size = this.size(); // return { // x: pos.x, // y: pos.y, // width: size.width, // height: size.height // }; }, topLeftRect: function () { var pos = this.topLeftPosition(); var size = this.size(); return { x: pos.x, y: pos.y, width: size.width, height: size.height }; }, isOffscreen: function () { return isOffscreen(this.rect()); } }; var PonyInstance = function PonyInstance (pony) { this.pony = pony; this.img = this.createImage(); this.clear(); }; PonyInstance.prototype = extend(new Instance(), { createImage: function () { var touch = function(evt) { evt.preventDefault(); if (evt.touches.length > 1 || (evt.type === "touchend" && evt.touches.length > 0)) return; var newEvt = document.createEvent("MouseEvents"); var type = null; var touch = null; switch (evt.type) { case "touchstart": type = "mousedown"; touch = evt.changedTouches[0]; break; case "touchmove": type = "mousemove"; touch = evt.changedTouches[0]; break; case "touchend": type = "mouseup"; touch = evt.changedTouches[0]; break; } newEvt.initMouseEvent(type, true, true, evt.target.ownerDocument.defaultView, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, evt.ctrlKey, evt.altKey, evt.shiftKey, evt.metaKey, 0, null); evt.target.dispatchEvent(newEvt); }; return tag('img', { draggable: 'false', style: { position: "fixed", userSelect: "none", borderStyle: "none", margin: "0", padding: "0", backgroundColor: "transparent", zIndex: String(BaseZIndex) }, ondragstart: function (event) { event.preventDefault(); }, ontouchstart: touch, ontouchmove: touch, ontouchend: touch, ondblclick: function () { // debug output var pos = this.position(); var duration = (this.end_time - this.start_time) / 1000; console.log( format('%s does %s%s for %.2f seconds, is at %d x %d and %s. See:', this.pony.name, this.current_behavior.name, this.current_behavior === this.paint_behavior ? '' : ' using '+this.paint_behavior.name, duration, pos.x, pos.y, (this.following ? 'follows '+this.following.name() : format('wants to go to %d x %d', this.dest_position.x, this.dest_position.y))), this); }.bind(this), onmousedown: function (event) { // IE 9 supports event.buttons and handles event.button like the w3c says. // IE <9 does not support event.buttons but sets event.button to the value // event.buttons should have (which is not what the w3c says). if ('buttons' in event ? event.buttons & 1 : (IE ? event.button & 1 : event.button === 0)) { dragged = this; this.mouseover = true; // timer === null means paused/not running if (timer !== null) { this.nextBehavior(true); } event.preventDefault(); } }.bind(this), onmouseover: function () { if (!this.mouseover) { this.mouseover = true; // timer === null means paused/not runnung if (timer !== null && !this.isMouseOverOrDragging() && (this.canMouseOver() || this.canDrag())) { this.nextBehavior(true); } } }.bind(this), onmouseout: function (event) { var target = event.target; // XXX: the img has no descendants but if it had it might still be correct in case // the relatedTarget is an anchester of the img or any node that is not a child // of img or img itself. // if (this.mouseover && (target === this.img || !descendantOf(target, this.img))) { if (this.mouseover) { this.mouseover = false; } }.bind(this) }); }, isMouseOverOrDragging: function () { return this.current_behavior && (this.current_behavior.movement === AllowedMoves.MouseOver || this.current_behavior.movement === AllowedMoves.Dragged); }, canDrag: function () { if (!this.current_behavior) { return this.pony.dragged_behaviors.length > 0; } else { var current_group = this.current_behavior.group; for (var i = 0, n = this.pony.dragged_behaviors.length; i < n; ++ i) { var behavior = this.pony.dragged_behaviors[i]; if (behavior.group === 0 || behavior.group === current_group) { return true; } } return false; } }, canMouseOver: function () { if (!this.current_behavior) { return this.pony.mouseover_behaviors.length > 0; } else { var current_group = this.current_behavior.group; for (var i = 0, n = this.pony.mouseover_behaviors.length; i < n; ++ i) { var behavior = this.pony.mouseover_behaviors[i]; if (behavior.group === 0 || behavior.group === current_group) { return true; } } return false; } }, name: function () { return this.pony.name; }, unspawn: function () { var currentTime = Date.now(); if (this.effects) { for (var i = 0, n = this.effects.length; i < n; ++ i) { removing.push({ at: currentTime, element: this.effects[i].img }); } } removing.push({ at: currentTime, element: this.img }); removeAll(this.pony.instances,this); removeAll(instances,this); }, clear: function () { if (this.effects) { for (var i = 0, n = this.effects.length; i < n; ++ i) { this.effects[i].clear(); } } if (this.img.parentNode) { this.img.parentNode.removeChild(this.img); } this.mouseover = false; this.start_time = null; this.end_time = null; this.current_interaction = null; this.interaction_targets = null; this.current_imgurl = null; this.interaction_wait = 0; this.current_position = {x: 0, y: 0}; this.dest_position = {x: 0, y: 0}; this.current_size = {width: 0, height: 0}; this.current_center = {x: 0, y: 0}; this.zIndex = BaseZIndex; this.current_behavior = null; this.paint_behavior = null; this.facing_right = true; this.end_at_dest = false; this.effects = []; this.repeating = []; }, interact: function (currentTime,interaction,targets) { var pony, behavior = randomSelect(interaction.behaviors); this.behave(this.pony.behaviors_by_name[behavior]); for (var i = 0, n = targets.length; i < n; ++ i) { pony = targets[i]; pony.behave(pony.pony.behaviors_by_name[behavior]); pony.current_interaction = interaction; } this.current_interaction = interaction; this.interaction_targets = targets; }, speak: function (currentTime,speech) { if (dontSpeak) return; if (speech.text) { var duration = Math.max(speech.text.length * 150, 1000); var remove = {at: currentTime + duration}; var text = tag('div',{ ondblclick: function () { remove.at = Date.now(); }, style: { fontSize: "14px", color: "#294256", background: IE ? "white" : "rgba(255,255,255,0.8)", position: "fixed", visibility: "hidden", margin: "0", padding: "4px", maxWidth: "250px", textAlign: "center", borderRadius: "10px", MozBorderRadius: "10px", width: 'auto', height: 'auto', boxShadow: "2px 2px 12px rgba(0,0,0,0.4)", MozBoxShadow: "2px 2px 12px rgba(0,0,0,0.4)", zIndex: String(BaseZIndex + 9000) }}, speech.text); remove.element = text; var rect = this.topLeftRect(); getOverlay().appendChild(text); var x = Math.round(rect.x + rect.width * 0.5 - text.offsetWidth * 0.5); var y = rect.y + rect.height; text.style.left = x+'px'; text.style.top = y+'px'; text.style.visibility = ''; removing.push(remove); text = null; } if (HasAudio && audioEnabled && speech.files) { var audio = createAudio(speech.files); audio.volume = volume; audio.play(); } }, update: function (currentTime, passedTime, winsize) { var curr = this.rect(); var dest = null; var dist; if (this.following) { if (this.following.img.parentNode) { dest = this.dest_position; dest.x = this.following.current_position.x; if (this.following.facing_right) { dest.x += this.current_behavior.x - this.following.paint_behavior.rightcenter.x; // dest.x += this.current_behavior.x - this.following.paint_behavior.rightcenter.x + 40; // dest.x += -this.following.paint_behavior.rightcenter.x + 50; } else { dest.x += -this.current_behavior.x + this.following.paint_behavior.leftcenter.x; // dest.x += -this.current_behavior.x + this.following.paint_behavior.leftcenter.x - 20; // dest.x += this.following.paint_behavior.leftcenter.x - 30; } dest.y = this.following.current_position.y + this.current_behavior.y; dist = distance(curr, dest); if (!this.current_behavior.x && !this.current_behavior.y && dist <= curr.width * 0.5) { dest = null; } } else { this.following = null; } } else { dest = this.dest_position; if (dest) dist = distance(curr, dest); } var pos; if (dest) { var dx = dest.x - curr.x; var dy = dest.y - curr.y; var tdist = this.current_behavior.speed * passedTime * 0.01 * globalSpeed; if (tdist >= dist) { pos = dest; } else { var scale = tdist / dist; pos = { x: Math.round(curr.x + scale * dx), y: Math.round(curr.y + scale * dy) }; } if (pos.x !== dest.x) { this.setFacingRight(pos.x <= dest.x); } else if (this.following) { if (this.current_behavior.auto_select_images) { // TODO: mechanism for selecting behavior for current movement } else if (Math.round(tdist) === 0) { if (this.current_behavior.stopped) { this.paint_behavior = this.current_behavior.stopped; } } else { if (this.current_behavior.moving) { this.paint_behavior = this.current_behavior.moving; } } this.setFacingRight(this.following.facing_right); } this.setPosition(pos); /* console.log( "current: "+curr.x+" x "+curr.y+ ", step: "+pos.x+" x "+pos.y+ ", dest: "+dest.x+" x "+dest.y+ ", dist: "+dist+ ", dist for passed time: "+tdist); */ } else { pos = curr; } // update associated effects: for (var i = 0; i < this.effects.length;) { var effect = this.effects[i]; if (effect.update(currentTime, passedTime, winsize)) { ++ i; } else { this.effects.splice(i, 1); removing.push({ element: effect.img, at: currentTime }); } } // check if some effects need to be repeated: for (var i = 0, n = this.repeating.length; i < n; ++ i) { var what = this.repeating[i]; if (what.at <= currentTime) { var inst = new EffectInstance(this, currentTime, what.effect); overlay.appendChild(inst.img); inst.updatePosition(currentTime, 0); this.effects.push(inst); what.at += what.effect.delay * 1000; } } if (this.interaction_wait <= currentTime && this.pony.interactions.length > 0 && !this.current_interaction) { var sumprob = 0; var interactions = []; var interaction = null; for (var i = 0, n = this.pony.interactions.length; i < n; ++ i) { interaction = this.pony.interactions[i]; var targets = interaction.reachableTargets(curr); if (targets) { sumprob += interaction.probability; interactions.push([interaction, targets]); } } if (interactions.length > 0) { var dice = Math.random() * sumprob; var diceiter = 0; for (var i = 0, n = interactions.length; i < n; ++ i) { interaction = interactions[i]; diceiter += interaction.probability; if (dice <= diceiter) { break; } } // The probability is meant for an execution evere 100ms, // but I use a configurable interaction interval. dice = Math.random() * (100 / interactionInterval); if (dice <= interaction[0].probability) { this.interact(currentTime,interaction[0],interaction[1]); return; } } this.interaction_wait += interactionInterval; } if (currentTime >= this.end_time || (this.end_at_dest && this.dest_position.x === pos.x && this.dest_position.y === pos.y)) { this.nextBehavior(); return; } if (this.following) return; var x1 = this.current_center.x; var y1 = this.current_center.y; var x2 = this.current_size.width - x1; var y2 = this.current_size.height - y1; var left = pos.x - x1; var right = pos.x + x2; var top = pos.y - y1; var bottom = pos.y + y2; // bounce of screen edges if (left <= 0) { if (this.dest_position.x < pos.x) { this.dest_position.x = Math.round(Math.max(pos.x + pos.x - this.dest_position.x, x1)); } } else if (right >= winsize.width) { if (this.dest_position.x > pos.x) { this.dest_position.x = Math.round(Math.min(pos.x + pos.x - this.dest_position.x, winsize.width - x2)); } } if (top <= 0) { if (this.dest_position.y < pos.y) { this.dest_position.y = Math.round(Math.max(pos.y + pos.y - this.dest_position.y, y1)); } } else if (bottom >= winsize.height) { if (this.dest_position.y > pos.y) { this.dest_position.y = Math.round(Math.min(pos.y + pos.y - this.dest_position.y, winsize.height - y2)); } } }, getNearestInstance: function (name) { var nearObjects = []; var pos = this.position(); var pony = ponies[name]; if (!pony) { for (var i = 0, n = instances.length; i < n; ++ i) { var inst = instances[i]; if (!this.loops(inst)) { for (var j = 0, m = inst.effects.length; j < m; ++ j) { var effect = inst.effects[j]; if (effect.effect.name === name) { nearObjects.push([distance(pos, effect.position()), effect]); } } } } } else { for (var i = 0, n = pony.instances.length; i < n; ++ i) { var inst = pony.instances[i]; if (!this.loops(inst)) { nearObjects.push([distance(pos, inst.position()), inst]); } } } if (nearObjects.length === 0) { return null; } nearObjects.sort(function (lhs,rhs) { return lhs[0] - rhs[0]; }); return nearObjects[0][1]; }, nextBehavior: function (breaklink) { var offscreen = this.isOffscreen(); if (!breaklink && this.current_behavior && this.current_behavior.linked) { this.behave(this.current_behavior.linked, offscreen); } else { if (this.current_interaction) { var currentTime = Date.now(); this.interaction_wait = currentTime + this.current_interaction.delay * 1000; if (this.interaction_targets) { // XXX: should I even do this or should I just let the targets do it? // they do it anyway, because current_interaction is also set for them // if it wouldn't be set, they could break out of interactions for (var i = 0, n = this.interaction_targets.length; i < n; ++ i) { this.interaction_targets[i].interaction_wait = this.interaction_wait; } this.interaction_targets = null; } this.current_interaction = null; } this.behave(this.randomBehavior(offscreen), offscreen); } }, setFacingRight: Gecko ? function (value) { this.facing_right = value; var newimg; if (value) { newimg = this.paint_behavior.rightimage; this.current_size = this.paint_behavior.rightsize; this.current_center = this.paint_behavior.rightcenter; } else { newimg = this.paint_behavior.leftimage; this.current_size = this.paint_behavior.leftsize; this.current_center = this.paint_behavior.leftcenter; } if (newimg !== this.current_imgurl) { // gif animation bug workaround var img = this.createImage(); img.style.left = this.img.style.left; img.style.top = this.img.style.top; img.style.zIndex = this.img.style.zIndex; img.src = this.current_imgurl = newimg; this.img.parentNode.replaceChild(img, this.img); this.img = img; } } : function (value) { this.facing_right = value; var newimg; if (value) { newimg = this.paint_behavior.rightimage; this.current_size = this.paint_behavior.rightsize; this.current_center = this.paint_behavior.rightcenter; } else { newimg = this.paint_behavior.leftimage; this.current_size = this.paint_behavior.leftsize; this.current_center = this.paint_behavior.leftcenter; } if (newimg !== this.current_imgurl) { this.img.src = this.current_imgurl = newimg; } }, behave: function (behavior, moveIntoScreen) { this.start_time = Date.now(); var duration = (behavior.minduration + (behavior.maxduration - behavior.minduration) * Math.random()); this.end_time = this.start_time + duration * 1000; var previous_behavior = this.current_behavior; this.current_behavior = this.paint_behavior = behavior; var neweffects = []; for (var i = 0, n = this.effects.length; i < n; ++ i) { var inst = this.effects[i]; if (inst.effect.duration) { neweffects.push(inst); } else { removing.push({ element: inst.img, at: this.start_time }); } } // get new image + size if (this.facing_right) { this.current_size = this.paint_behavior.rightsize; this.current_center = this.paint_behavior.rightcenter; } else { this.current_size = this.paint_behavior.leftsize; this.current_center = this.paint_behavior.leftcenter; } var spoke = false; if (previous_behavior && previous_behavior.speakend) { this.speak(this.start_time, previous_behavior.speakend); spoke = true; } this.following = null; if (behavior.follow) { this.following = this.getNearestInstance(behavior.follow); } if (behavior.speakstart) { this.speak(this.start_time, behavior.speakstart); } else if (!spoke && !this.following && !this.current_interaction) { this.speakRandom(this.start_time, speakProbability); } var pos = this.position(); var size = this.size(); var winsize = windowSize(); this.end_at_dest = false; if (this.following) { this.dest_position.x = this.following.current_position.x; this.dest_position.y = this.following.current_position.y; } else if (!behavior.follow && (behavior.x || behavior.y)) { this.end_at_dest = true; this.dest_position = { x: Math.round((winsize.width - size.width) * (behavior.x || 0) / 100), y: Math.round((winsize.height - size.height) * (behavior.y || 0) / 100) }; } else { // reduce chance of going off-screen var movements = null; switch (behavior.movement) { case AllowedMoves.HorizontalOnly: movements = [Movements.Left, Movements.Right]; break; case AllowedMoves.VerticalOnly: movements = [Movements.Up, Movements.Down]; break; case AllowedMoves.HorizontalVertical: movements = [Movements.Left, Movements.Right, Movements.Up, Movements.Down]; break; case AllowedMoves.DiagonalOnly: movements = [Movements.UpLeft, Movements.UpRight, Movements.DownLeft, Movements.DownRight]; break; case AllowedMoves.DiagonalHorizontal: movements = [Movements.Left, Movements.Right, Movements.UpLeft, Movements.UpRight, Movements.DownLeft, Movements.DownRight]; break; case AllowedMoves.DiagonalVertical: movements = [Movements.Up, Movements.Down, Movements.UpLeft, Movements.UpRight, Movements.DownLeft, Movements.DownRight]; break; case AllowedMoves.All: movements = [Movements.Left, Movements.Right, Movements.Up, Movements.Down, Movements.UpLeft, Movements.UpRight, Movements.DownLeft, Movements.DownRight]; break; } if (movements === null) { this.dest_position.x = Math.round(pos.x); this.dest_position.y = Math.round(pos.y); } else { var nearTop = pos.y - size.height * 0.5 < 100; var nearBottom = pos.y + size.height * 0.5 + 100 > winsize.height; var nearLeft = pos.x - size.width * 0.5 < 100; var nearRight = pos.x + size.width * 0.5 + 100 > winsize.width; var reducedMovements = movements.slice(); if (nearTop) { removeAll(reducedMovements, Movements.Up); removeAll(reducedMovements, Movements.UpLeft); removeAll(reducedMovements, Movements.UpRight); } if (nearBottom) { removeAll(reducedMovements, Movements.Down); removeAll(reducedMovements, Movements.DownLeft); removeAll(reducedMovements, Movements.DownRight); } if (nearLeft) { removeAll(reducedMovements, Movements.Left); removeAll(reducedMovements, Movements.UpLeft); removeAll(reducedMovements, Movements.DownLeft); } if (nearRight) { removeAll(reducedMovements, Movements.Right); removeAll(reducedMovements, Movements.UpRight); removeAll(reducedMovements, Movements.DownRight); } // speed is in pixels/100ms, duration is in sec var dist = behavior.speed * duration * 100 * globalSpeed; var a; switch (randomSelect(reducedMovements.length === 0 ? movements : reducedMovements)) { case Movements.Up: this.dest_position = { x: pos.x, y: pos.y - dist }; break; case Movements.Down: this.dest_position = { x: pos.x, y: pos.y + dist }; break; case Movements.Left: this.dest_position = { x: pos.x - dist, y: pos.y }; break; case Movements.Right: this.dest_position = { x: pos.x + dist, y: pos.y }; break; case Movements.UpLeft: a = Math.sqrt(dist*dist*0.5); this.dest_position = { x: pos.x - a, y: pos.y - a }; break; case Movements.UpRight: a = Math.sqrt(dist*dist*0.5); this.dest_position = { x: pos.x + a, y: pos.y - a }; break; case Movements.DownLeft: a = Math.sqrt(dist*dist*0.5); this.dest_position = { x: pos.x - a, y: pos.y + a }; break; case Movements.DownRight: a = Math.sqrt(dist*dist*0.5); this.dest_position = { x: pos.x + a, y: pos.y + a }; break; } if (moveIntoScreen) { this.dest_position = clipToScreen(extend(this.dest_position, size)); this.end_at_dest = true; } else { // clipToScreen already rounds this.dest_position.x = Math.round(this.dest_position.x); this.dest_position.y = Math.round(this.dest_position.y); } } } // this changes the image to the new behavior: this.setFacingRight( pos.x !== this.dest_position.x ? pos.x <= this.dest_position.x : this.facing_right); // this initializes the new images position: // (alternatively maybe this.update(...) could be called?) this.setPosition(this.current_position); var overlay = getOverlay(); this.repeating = []; for (var i = 0, n = behavior.effects.length; i < n; ++ i) { var effect = behavior.effects[i]; var inst = new EffectInstance(this, this.start_time, effect); overlay.appendChild(inst.img); inst.updatePosition(this.start_time, 0); neweffects.push(inst); if (effect.delay) { this.repeating.push({ effect: effect, at: this.start_time + effect.delay * 1000 }); } } this.effects = neweffects; /* var msg; if (this.following) { msg = "following "+behavior.follow; } else { if (this.dest_position.x !== pos.x || this.dest_position.y !== pos.y) { msg = "move from "+pos.x+" x "+pos.y+" to "+ Math.round(this.dest_position.x)+" x "+ Math.round(this.dest_position.y); } else { msg = "no movement"; } if (behavior.follow) { msg += " (wanted to follow "+behavior.follow+")"; } } console.log(this.pony.name+" does "+behavior.name+": "+msg+" in "+duration+ " seconds"); */ }, teleport: function () { var winsize = windowSize(); var size = this.size(); this.setTopLeftPosition({ x: Math.random() * (winsize.width - size.width), y: Math.random() * (winsize.height - size.height) }); }, speakRandom: function (start_time, speak_probability) { if (Math.random() >= speak_probability) return; var filtered = []; var current_group = this.current_behavior.group; for (var i = 0, n = this.pony.random_speeches.length; i < n; ++ i) { var speech = this.pony.random_speeches[i]; if (speech.group === 0 || speech.group === current_group) { filtered.push(speech); } } if (filtered.length > 0) { this.speak(start_time, randomSelect(filtered)); } }, randomBehavior: function (forceMovement) { var behaviors; var current_group = this.current_behavior ? this.current_behavior.group : 0; if (this === dragged && this.canDrag()) { behaviors = this.pony.dragged_behaviors; } else if (this.mouseover && this.canMouseOver()) { behaviors = this.pony.mouseover_behaviors; } else { behaviors = this.pony.random_behaviors; } var sumprob = 0; var filtered = []; for (var i = 0, n = behaviors.length; i < n; ++ i) { var behavior = behaviors[i]; // don't filter looping behaviors because getNearestInstance filteres // looping instances and then it just degrades to a standard behavior if (forceMovement && !behavior.isMoving()) continue; if (current_group !== 0 && behavior.group !== 0 && behavior.group !== current_group) continue; sumprob += behavior.probability; filtered.push(behavior); } var dice = Math.random() * sumprob; var diceiter = 0; for (var i = 0, n = filtered.length; i < n; ++ i) { var behavior = filtered[i]; diceiter += behavior.probability; if (dice <= diceiter) { return behavior; } } return forceMovement ? this.randomBehavior(false) : null; }, loops: function (instance) { while (instance) { if (this === instance) return true; instance = instance.following; } return false; } }); var EffectInstance = function EffectInstance (pony, start_time, effect) { this.pony = pony; this.start_time = start_time; var duration = effect.duration * 1000; // XXX: Gecko gif animations speed is buggy! if (Gecko) duration *= 0.6; duration = Math.max(duration - fadeDuration, fadeDuration); this.end_time = start_time + duration; this.effect = effect; var imgurl; if (pony.facing_right) { imgurl = this.effect.rightimage; this.current_size = this.effect.rightsize; this.current_center = this.effect.rightcenter_point; } else { imgurl = this.effect.leftimage; this.current_size = this.effect.leftsize; this.current_center = this.effect.leftcenter_point; } this.current_position = {x: 0, y: 0}; this.zIndex = BaseZIndex; this.current_imgurl = null; this.img = this.createImage(imgurl); var locs = ['rightloc','rightcenter','leftloc','leftcenter']; for (var i = 0, n = locs.length; i < n; ++ i) { var name = locs[i]; var loc = effect[name]; if (loc === Locations.Any) { loc = randomSelect([ Locations.Top, Locations.Bottom, Locations.Left, Locations.Right, Locations.BottomRight, Locations.BottomLeft, Locations.TopRight, Locations.TopLeft, Locations.Center ]); } else if (loc === Locations.AnyNotCenter) { loc = randomSelect([ Locations.Top, Locations.Bottom, Locations.Left, Locations.Right, Locations.BottomRight, Locations.BottomLeft, Locations.TopRight, Locations.TopLeft ]); } this[name] = loc; } }; EffectInstance.prototype = extend(new Instance(), { createImage: function (src) { var img = tag(Gecko || Opera ? 'img' : 'iframe', { src: src, draggable: 'false', style: { position: "fixed", overflow: "hidden", userSelect: "none", pointerEvents: "none", borderStyle: "none", margin: "0", padding: "0", backgroundColor: "transparent", width: this.current_size.width+"px", height: this.current_size.height+"px", zIndex: String(BaseZIndex) }}); if (IE) { img.setAttribute("scrolling", "no"); img.setAttribute("frameborder", "0"); img.setAttribute("marginheight", "0"); img.setAttribute("marginwidth", "0"); } return img; }, name: function () { return this.effect.name; }, clear: function () { if (this.img.parentNode) { this.img.parentNode.removeChild(this.img); } }, updatePosition: function (currentTime, passedTime) { var loc, center; if (this.pony.facing_right) { loc = this.rightloc; center = this.rightcenter; } else { loc = this.leftloc; center = this.leftcenter; } var size = this.size(); var pos; switch (center) { case Locations.Top: pos = {x: -size.width * 0.5, y: 0}; break; case Locations.Bottom: pos = {x: -size.width * 0.5, y: -size.height}; break; case Locations.Left: pos = {x: 0, y: -size.height * 0.5}; break; case Locations.Right: pos = {x: -size.width, y: -size.height * 0.5}; break; case Locations.BottomRight: pos = {x: -size.width, y: -size.height}; break; case Locations.BottomLeft: pos = {x: 0, y: -size.height}; break; case Locations.TopRight: pos = {x: -size.width, y: 0}; break; case Locations.TopLeft: pos = {x: 0, y: 0}; break; case Locations.Center: pos = {x: -size.width * 0.5, y: -size.height * 0.5}; break; } var ponyRect = this.pony.topLeftRect(); switch (loc) { case Locations.Top: pos.x += ponyRect.x + ponyRect.width * 0.5; pos.y += ponyRect.y; break; case Locations.Bottom: pos.x += ponyRect.x + ponyRect.width * 0.5; pos.y += ponyRect.y + ponyRect.height; break; case Locations.Left: pos.x += ponyRect.x; pos.y += ponyRect.y + ponyRect.height * 0.5; break; case Locations.Right: pos.x += ponyRect.x + ponyRect.width; pos.y += ponyRect.y + ponyRect.height * 0.5; break; case Locations.BottomRight: pos.x += ponyRect.x + ponyRect.width; pos.y += ponyRect.y + ponyRect.height; break; case Locations.BottomLeft: pos.x += ponyRect.x; pos.y += ponyRect.y + ponyRect.height; break; case Locations.TopRight: pos.x += ponyRect.x + ponyRect.width; pos.y += ponyRect.y; break; case Locations.TopLeft: pos.x += ponyRect.x; pos.y += ponyRect.y; break; case Locations.Center: pos.x += ponyRect.x + ponyRect.width * 0.5; pos.y += ponyRect.y + ponyRect.height * 0.5; break; } this.setTopLeftPosition(pos); }, /* setImage: function (url) { if (this.current_imgurl !== url) { this.img.src = dataUrl('text/html', '