Index: binaries/data/mods/mod/gui/common/animateGUI.js =================================================================== --- /dev/null +++ binaries/data/mods/mod/gui/common/animateGUI.js @@ -0,0 +1,491 @@ +/** + * Simple GUI animator addon + * guiObject can be a string or a GUI object + * Use: + * animateObject (guiObject, settings) + * animateObject.complete (guiObject, [completeQueue]) + * animateObject.end (guiObject, [endQueue]) + * animateObject.chain (guiObject, chainSettingsList, [defaultSettings]) + * + * Example of settings: + * + * let settings = { + * "color" : "255 255 255 12", + * "color" : { "r": 255, "g": 255, "b": 255, "a": 12 }, + * "textcolor" : "140 140 140 28", + * "textcolor" : { "r": 140, "g": 140, "b": 140, "a": 28}, + * "size" : "rleft%left rtop%top rright%right rbottom%bottom", + * "size" : { + * "left" : 0, "top" : 10, "right" : 250, "bottom" : 300, + * "rleft": 50, "rtop": 50, "rright": 50, "rbottom": 100, + * }, + * "duration" : 1000, + * "delay" : 100, + * "curve" : "linear", + * "onStart" : (guiObject) => { warn("animation of " + + * guiObject.name + " has started"); }, + * "onTick" : (guiObject) => { warn("animation has ticked"); }, + * "onComplete" : (guiObject) => { warn("animation has completed"); }, + * "onStartNoDelay": false, + * "onTickNoDelay" : false, + * "queue" : false, + * }; + * + * - "textcolor" only works with objects that can have text. + * - "color" only works if the object parameter sprite is defined. + * as sprite = "color: R G B A" + * - "size" always works. + * + * - AnimateGUIObject.default for defaults that can be changed + * - AnimateGUIObject.curves for animation transitions types="string" + * that can be changed + * + * Each setting can be set independent from the other (including + * individual values of each setting). + * In case of two animations with the same setting the new animation + * setting will overrite the old one. + */ + +function AnimateGUIObject(guiObject, settings) +{ + this.guiObject = guiObject; + this.settings = deepfreeze(settings); + this.queue = clone(settings.queue === undefined ? + AnimateGUIObject.default.queue : + this.settings.queue); + this.values = {}; + this.values.delay = clone(this.settings.delay === undefined ? + AnimateGUIObject.default.delay : + Math.max(0, this.settings.delay)); + this.values.duration = clone(this.settings.duration === undefined ? + AnimateGUIObject.default.duration : + Math.max(0, this.settings.duration)); + /** + * Stores if something has already been marked as done. + */ + this.done = {}; + /** + * Stores the settings parsed data, filled when an + * AnimateGUIObject.prototype.run is called. + */ + this.properties = {}; + /** + * Stores the settings parsed actions, filled when an + * AnimateGUIObject.prototype.onStart is called. + */ + this.attributes = {}; + for (let identity in this.identity) + this.parseSettingsProperty(identity); + return this; +} + +AnimateGUIObject.default = { + "duration": 150, + "curve": "ease-in-out", + "delay": 0, + "queue": false +}; + +AnimateGUIObject.curves = { + "ease-in-out": x => x * x * x * (x * (x * 6 - 15) + 10), + "ease-in-out-2": x => 3 * x * x * (1 - x) + x * x * x, + "linear": x => x +}; + +/** + * All the types of properties can can be animated + */ +AnimateGUIObject.prototype.identity = {}; + +/** + * Filled when AnimateGUIObject.prototype.run is first called + */ +AnimateGUIObject.prototype.parseSettingsDefaults = function () +{ + if (this.done.parseSettingsDefaults) + return; + + this.done.parseSettingsDefaults = true; + let time = Date.now(); + this.name = this.guiObject.name; + this.values.onStart = this.settings.onStart === undefined ? + () => { } : + this.settings.onStart; + + this.values.start = time + this.values.delay; + this.values.end = this.values.start + this.values.duration; + this.values.curve = clone(this.settings.curve === undefined || + AnimateGUIObject.curves[this.settings.curve] === undefined ? + AnimateGUIObject.default.curve : + this.settings.curve); + + this.values.curveFunction = AnimateGUIObject.curves[this.values.curve]; + this.running = time < this.values.end; + this.started = time > this.values.start; +}; + +/** + * Parse property and make sure that the final object is a copy not a reference + */ +AnimateGUIObject.prototype.parseSettingsProperty = function (property) +{ + if (this.settings[property] != undefined) + this.properties[property] = typeof this.settings[property] == "string" ? + this.identity[property].toObject(this.settings[property]) : + Object.assign({}, this.settings[property]); +}; + +AnimateGUIObject.prototype.parseAction = function (property) +{ + if (this.properties[property] === undefined) + return; + + let attribute = { "parameters": {} }; + + let original = this.identity[property].get(this.guiObject); + this.identity[property].types. + filter(type => this.properties[property][type] != undefined). + forEach(type => + attribute.parameters[type] = x => + original[type] + x * + (this.properties[property][type] - original[type]) + ); + + attribute.calc = x => + { + let object = this.identity[property].get(this.guiObject); + Object.keys(attribute.parameters). + forEach(type => + object[type] = attribute.parameters[type](x) + ); + this.identity[property].set(this.guiObject, object); + } + + this.attributes[property] = attribute; +} + +/** + * Run for each tick + */ +AnimateGUIObject.prototype.run = function (time) +{ + this.parseSettingsDefaults(); + this.running = time < this.values.end; + this.started = time >= this.values.start; + this.onStart(); + this.update(time); + this.onTick(); + this.onComplete(); + return this.running; +}; + +/** + * Updates animation object attributes + */ +AnimateGUIObject.prototype.update = function (time) +{ + if (!this.started) + return; + + let uniformTime = this.running ? + (time - this.values.start) / this.values.duration : + 1; + let x = this.values.curveFunction(uniformTime); + + for (let attribute in this.attributes) + this.attributes[attribute].calc(x); +}; + +/** + * There can be onStart with/without delay and will always execute once + * per animation. + */ +AnimateGUIObject.prototype.onStart = function () +{ + if (!this.values.onStart + || (!this.started && !this.settings.onStartNoDelay) + || this.done.onStart) + return; + + this.values.onStart(this.guiObject, this); + this.done.onStart = true; + /** + * Parse action is called here given that this.values.onStart() could + * want to modify the object's initial values before the animation starts. + */ + for (let property in this.properties) + this.parseAction(property); +}; + +/** + * There can be onTick with/without delay or no onTick at all + * and will always execute once per animation at maximum. + */ +AnimateGUIObject.prototype.onTick = function () +{ + if (!this.settings.onTick + || (!this.started && !this.settings.onTickNoDelay)) + return; + + this.settings.onTick(this.guiObject, this); +}; + +/** + * There can be onComplete with/without delay or no onTick at all + * and will always execute once per animation at maximum. + */ +AnimateGUIObject.prototype.onComplete = function () +{ + if (!this.settings.onComplete + || this.running + || this.done.onComplete) + return; + + this.settings.onComplete(this.guiObject, this); + this.done.onComplete = true; +}; + +/** + * Checks if the animation has any attribute/property that still + * modifies the guiObject + */ +AnimateGUIObject.prototype.isAlive = function () +{ + return Object.keys(this.properties).length != 0; +} + +/** + * Removes properties/attributes from the old animation that the + * new animation has + */ +AnimateGUIObject.prototype.removeIntersections = function (newAnimateGUIObject) +{ + for (let property of Object.keys(this.properties)) + this.removePropertyIntersections(newAnimateGUIObject, property); + return this; +}; + +AnimateGUIObject.prototype.removePropertyIntersections = function (newAnimateGUIObject, property) +{ + if (this.properties[property] === undefined + || newAnimateGUIObject.properties[property] === undefined) + return; + + for (let type of Object.keys(this.properties[property])) + { + if (this.properties[property][type] === undefined + || newAnimateGUIObject.properties[property][type] === undefined) + continue; + + delete this.properties[property][type]; + if (this.attributes[property] != undefined + && this.attributes[property].parameters != undefined) + delete this.attributes[property].parameters[type]; + } + + if (Object.keys(this.properties[property]).length != 0) + return; + + delete this.properties[property]; + delete this.attributes[property]; +}; + +/** + * Jump to the end of the animation. + * onStart/onTick/onComplete behaviour doesn't change. + */ +AnimateGUIObject.prototype.complete = function () +{ + this.value.end = Date.now(); +} + +/** + * Manages all the animations + */ +function AnimateGUI() +{ + // key = objectName , value = [animation1,animation2,animation3,...] + // If a value is [] its entry should be deleted + this.running = new Map(); + this.queue = new Map(); +}; + +AnimateGUI.prototype.parseGUIObject = function (guiObject) +{ + return typeof guiObject == "string" ? + Engine.GetGUIObjectByName(guiObject) : + guiObject; +} + +AnimateGUI.prototype.addAnimation = function (guiObject, settings) +{ + let object = this.parseGUIObject(guiObject); + let newAnimation = new AnimateGUIObject(object, settings); + + if (!this.running.has(object.name)) + { + // If no animation running. + this.running.set(object.name, [newAnimation]); + this.queue.delete(object.name); + } + else if (!newAnimation.queue) + { + // If animation(s) running and new animation doesn't queue. + // Delete parts of the other animations that conflic with the new one. + let animations = this.running.get(object.name).filter(animation => + animation.removeIntersections(newAnimation).isAlive() + ); + animations.push(newAnimation); + this.running.set(object.name, animations); + this.queue.delete(object.name); + } + else + { + // If animaton(s) running and new animation does queue. + if (this.queue.has(object.name)) + this.queue.get(object.name).push(newAnimation); + else + this.queue.set(object.name, [newAnimation]); + } + return this; +}; + +AnimateGUI.prototype.animationsInQueue = function (objectName) +{ + if (this.running.has(objectName) + && !this.running.get(objectName).length) + this.running.delete(objectName); + + // If there are animations running + if (this.running.has(objectName)) + return false; + + // If there are no animations running and no animations pending + if (!this.queue.has(objectName)) + { + this.running.delete(objectName); + return false; + } + + let nextAnimation = this.queue.get(objectName).shift(); + + // If there are no animations running and animations pending + if (!nextAnimation) + { + this.queue.delete(objectName); + return false; + } + + this.running.set(objectName, [nextAnimation]); + return true; +} + +AnimateGUI.prototype.onTick = function () +{ + let time = Date.now(); + for (let name of this.running.keys()) + { + /** + * Repeat loop in case there are multiple animation + * queued that have delay:0 and duration:0 + * (AnimateGUI.prototype.complete case) + */ + do + { + this.running.set( + name, + this.running.get(name).filter(animation => animation.run(time)) + ); + } + while (this.animationsInQueue(name)) + } +}; + +AnimateGUI.prototype.complete = function (guiObject, completeQueue) +{ + let name = this.parseGUIObject(guiObject).name; + if (!this.running.has(name)) + return; + + for (let animation of this.running.get(name)) + animation.complete(); + + if (!completeQueue || !this.queue.has(name)) + return; + + for (let animation of this.queue.get(name)) + { + animation.values.delay = 0; + animation.values.duration = 0; + } + return this; +} + +AnimateGUI.prototype.end = function (guiObject, endQueue) +{ + let name = this.parseGUIObject(guiObject).name; + this.running.delete(name); + if (endQueue) + this.queue.delete(name); + return this; +} + +/** + * @param {Object | String} guiObject + * @param {Object} settings + * @param {Number} [settings.duration] + * @param {Number} [settings.delay] + * @param {String} [settings.curve] + * @param {Function} [settings.onStart] + * @param {Function} [settings.onTick] + * @param {Function} [settings.onComplete] + * @param {Boolean} [settings.onStartNoDelay] + * @param {Boolean} [settings.onTickNoDelay] + * @param {Boolean} [settings.queue] + * @param {{r,g,b,a} | String} [settings.color] + * @param {{r,g,b,a} | String} [settings.textcolor] + * @param {{left,top,right,bottom,rleft,rtop,rright,rbottom} | String} settings.size + */ +let animateObject = function (guiObject, settings) +{ + animateObject.gui.addAnimation(guiObject, settings); +} + +animateObject.onTick = function () +{ + animateObject.gui.onTick(); +} + +animateObject.gui = new AnimateGUI(); + +/** + * Ends animation as if had reached end time. + * onStart/onTick/onComplete called as usual. + * Optional argument to complete all remaining queues. + */ +animateObject.complete = function (guiObject, completeQueue) +{ + animateObject.gui.complete(guiObject, completeQueue); +} + +/** + * Ends animation at given time of command. + * onStart/onTick/onComplete not called. + * Optional argument to end all remaining queues. + */ +animateObject.end = function (guiObject, endQueue) +{ + animateObject.gui.end(guiObject, endQueue); +} + +/** + * Makes a chained animation + * @param {Object} guiObject + * @param {Object[]} chainSettingsList + * @param {Object} sharedSettings + */ +animateObject.chain = function (guiObject, chainSettingsList, sharedSettings) +{ + for (let settings of chainSettingsList) + animateObject(guiObject, Object.assign({}, sharedSettings || {}, settings)); +} Index: binaries/data/mods/mod/gui/common/animateGUI_color.js =================================================================== --- /dev/null +++ binaries/data/mods/mod/gui/common/animateGUI_color.js @@ -0,0 +1,37 @@ +AnimateGUIObject.prototype.identity.color = { + "types": deepfreeze(["r", "g", "b", "a"]), + "set": function (guiObject, object) + { + guiObject.sprite = "color: " + this.toString(object); + }, + "get": function (guiObject) + { + let color = this.toObject(guiObject.sprite.split(":")[1]); + if (color.a === undefined) + color.a = 1; + return color; + }, + "toObject": function (text) + { + let color = this.spaceSplit(text); + return color.length == 4 ? + { + "r": (+color[0]) / 255, + "g": (+color[1]) / 255, + "b": (+color[2]) / 255, + "a": (+color[3]) / 255 + } : + { + "r": (+color[0]) / 255, + "g": (+color[1]) / 255, + "b": (+color[2]) / 255 + }; + }, + "toString": object => + { + return object.a === undefined ? + GUIColor(object.r, object.g, object.b).toString() : + GUIColor(object.r, object.g, object.b, object.a).toString() + }, + "spaceSplit": text => text.split(" ").filter(t => !!t) +}; Index: binaries/data/mods/mod/gui/common/animateGUI_size.js =================================================================== --- /dev/null +++ binaries/data/mods/mod/gui/common/animateGUI_size.js @@ -0,0 +1,26 @@ +AnimateGUIObject.prototype.identity.size = { + "types": deepfreeze(["left", "top", "right", "bottom", "rleft", "rtop", "rright", "rbottom"]), + "set": (guiObject, object) => guiObject.size = object, + "get": guiObject => guiObject.size, + "toObject": function (text) + { + let types = this.types.slice(0, 4); + let rtypes = this.types.slice(4, 8); + let slist = this.spaceSplit(text).map(t => t.split("%")); + let size = {}; + types.forEach((type, index) => + { + let para = slist[index]; + if (para.length == 2) + { + size[rtypes[index]] = +para[0]; + if (para[1] != "") + size[type] = +para[1]; + } + else + size[type] = +para[0]; + }); + return size; + }, + "spaceSplit": text => text.split(" ").filter(t => !!t) +}; Index: binaries/data/mods/mod/gui/common/animateGUI_textcolor.js =================================================================== --- /dev/null +++ binaries/data/mods/mod/gui/common/animateGUI_textcolor.js @@ -0,0 +1,35 @@ +AnimateGUIObject.prototype.identity.textcolor = { + "types": deepfreeze(["r", "g", "b", "a"]), + "set": (guiObject, object) => + { + // For some strange reason seem color with value 1 is black + Object.keys(object).forEach(type => + object[type] = Math.min(object[type], 0.999) + ); + guiObject.textcolor = object + }, + "get": function (guiObject) + { + let color = guiObject.textcolor; + if (color.a === undefined) + color.a = 1; + return color; + }, + "toObject": function (text) + { + let color = this.spaceSplit(text); + return color.length == 4 ? + { + "r": (+color[0]) / 255, + "g": (+color[1]) / 255, + "b": (+color[2]) / 255, + "a": (+color[3]) / 255 + } : + { + "r": (+color[0]) / 255, + "g": (+color[1]) / 255, + "b": (+color[2]) / 255 + }; + }, + "spaceSplit": text => text.split(" ").filter(t => !!t) +};