/** * @module PIXI UI * @namespace springroll.pixi * @requires Core, PIXI Display */ (function(undefined) { // Import classes var Debug, Container = include('PIXI.Container'), Point = include('PIXI.Point'), Sprite = include('PIXI.Sprite'), BitmapText = include('PIXI.extras.BitmapText', false), Text = include('PIXI.Text'), Texture = include('PIXI.Texture'); /** * A Multipurpose button class. It is designed to have one image, and an optional text label. * The button can be a normal button or a selectable button. * The button functions similarly with both EaselJS and PIXI, but slightly differently in * initialization and callbacks. * Use the "buttonPress" and "buttonOver" events to know about button clicks and mouse overs, * respectively. * * @class Button * @extends PIXI.Container * @constructor * @param {Object} imageSettings Information about the art to be used for button states, as * well as if the button is selectable or not. * @param {Array} [imageSettings.priority=null] The state priority order. If omitted, defaults * to ["disabled", "down", "over", "up"]. Previous * versions of Button used a hard coded order: * ["highlighted", "disabled", "down", "over", * "selected", "up"]. * @param {Object|PIXI.Texture} [imageSettings.up] The texture for the up state of the button. * This can be either the texture itself, or an * object with 'tex' and 'label' properties. * @param {PIXI.Texture|String} [imageSettings.up.tex] The texture to use for the up state. If * this is a string, Texture.fromImage() * will be used. * @param {Object} [imageSettings.up.label=null] Label information specific to this state. * Properties on this parameter override data in * the label parameter for this button state * only. All values except "text" and "type" from * the label parameter may be overridden. * @param {Object|PIXI.Texture} [imageSettings.over=null] The texture for the over state of the * button. If omitted, uses the up * state. * @param {PIXI.Texture|String} [imageSettings.over.tex] The texture to use for the over state. * If this is a string, * Texture.fromImage() will be used. * @param {Object} [imageSettings.over.label=null] Label information specific to this state. * Properties on this parameter override data * in the label parameter for this button state * only. All values except "text" and "type" * from the label parameter may be overridden. * @param {Object|PIXI.Texture} [imageSettings.down=null] The texture for the down state of the * button. If omitted, uses the up * state. * @param {PIXI.Texture|String} [imageSettings.down.tex] The texture to use for the down state. * If this is a string, * Texture.fromImage() will be used. * @param {Object} [imageSettings.down.label=null] Label information specific to this state. * Properties on this parameter override data * in the label parameter for this button state * only. All values except "text" and "type" * from the label parameter may be overridden. * @param {Object|PIXI.Texture} [imageSettings.disabled=null] The texture for the disabled * state of the button. If omitted, * uses the up state. * @param {PIXI.Texture|String} [imageSettings.disabled.tex] The texture to use for the disabled * state. If this is a string, * Texture.fromImage() will be used. * @param {Object} [imageSettings.disabled.label=null] Label information specific to this * state. Properties on this parameter * override data in the label parameter for * this button state only. All values * except "text" and "type" from the label * parameter may be overridden. * @param {Object|PIXI.Texture} [imageSettings.<yourCustomState>=null] The visual information * about a custom state * found in * imageSettings.priority. * Any state added this way * has a property of the * same name added to the * button. Examples of * previous states that * have been * moved to this system are * "selected" and * "highlighted". * @param {PIXI.Texture|String} [imageSettings.<yourCustomState>.tex] The texture to use for * your custom state. If * this is a string, * Texture.fromImage() * will be used. * @param {Object} [imageSettings.<yourCustomState>.label=null] Label information specific to * this state. Properties on this * parameter override data in the * label parameter for this button * state only. All values except * "text" from the label parameter * may be overridden. * @param {PIXI.Point} [imageSettings.origin=null] An optional offset for all button graphics, * in case you want button positioning to not * include a highlight glow, or any other * reason you would want to offset the button * art and label. * @param {Number} [imageSettings.scale=1] The scale to use for the textures. This allows * smaller art assets than the designed size to be * used. * @param {Object} [label=null] Information about the text label on the button. Omitting this * makes the button not use a label. * @param {String} [label.type] If label.type is "bitmap", then a PIXI.extras.BitmapText text * is created, otherwise a PIXI.Text is created for the label. * @param {String} [label.text] The text to display on the label. * @param {Object} [label.style] The style of the text field, in the format that * PIXI.extras.BitmapText and PIXI.Text expect. * @param {String|Number} [label.x="center"] An x position to place the label text at relative * to the button. * @param {String|Number} [label.y="center"] A y position to place the label text at relative * to the button. If omitted, "center" is used, which * attempts to vertically center the label on the * button. * @param {Boolean} [enabled=true] Whether or not the button is initially enabled. */ var Button = function(imageSettings, label, enabled) { Debug = include('springroll.Debug', false); if (!imageSettings && DEBUG) { throw "springroll.pixi.Button requires image as first parameter"; } Container.call(this); /** * The sprite that is the body of the button. * @property {PIXI.Sprite} back * @readOnly */ this.back = new Sprite(); /** * The text field of the button. The label is centered by both width and height on the * button. * @property {PIXI.Text|PIXI.BitmapText} label * @readOnly */ this.label = null; /** * A dictionary of state booleans, keyed by state name. * @private * @property {Object} _stateFlags */ this._stateFlags = {}; /** * An array of state names (Strings), in their order of priority. * The standard order previously was ["highlighted", "disabled", "down", "over", * "selected", "up"]. * @private * @property {Array} _statePriority */ this._statePriority = imageSettings.priority || DEFAULT_PRIORITY; /** * A dictionary of state graphic data, keyed by state name. * Each object contains the sourceRect (src) and optionally 'trim', another Rectangle. * Additionally, each object will contain a 'label' object if the button has a text label. * @private * @property {Object} _stateData */ this._stateData = null; /** * The current style for the label, to avoid setting this if it is unchanged. * @private * @property {Object} _currentLabelStyle */ this._currentLabelStyle = null; /** * An offset to button positioning, generally used to adjust for a highlight * around the button. * @private * @property {PIXI.Point} _offset */ this._offset = new Point(); /** * The width of the button art, independent of the scaling of the button itself. * @private * @property {Number} _width */ this._width = 0; /** * The height of the button art, independent of the scaling of the button itself. * @private * @property {Number} _height */ this._height = 0; this.addChild(this.back); this._onOver = this._onOver.bind(this); this._onOut = this._onOut.bind(this); this._onDown = this._onDown.bind(this); this._onUp = this._onUp.bind(this); this._onUpOutside = this._onUpOutside.bind(this); this._emitPress = this._emitPress.bind(this); var _stateData = this._stateData = {}; //a clone of the label data to use as a default value, without changing the original var labelData; if (label) { labelData = clone(label); delete labelData.text; delete labelData.type; if (labelData.x === undefined) labelData.x = "center"; if (labelData.y === undefined) labelData.y = "center"; //clone the style object and set up the defaults from PIXI.Text or PIXI.BitmapText var style = labelData.style = clone(label.style); if (label.type == "bitmap") { style.align = style.align || "left"; } else { style.font = style.font || "bold 20pt Arial"; style.fill = style.fill || "black"; style.align = style.align || "left"; style.stroke = style.stroke || "black"; style.strokeThickness = style.strokeThickness || 0; style.wordWrap = style.wordWrap || false; style.wordWrapWidth = style.wordWrapWidth || 100; } } //start at the end to start at the up state for (var i = this._statePriority.length - 1; i >= 0; --i) { var state = this._statePriority[i]; //set up the property for the state so it can be set // - the function will ignore reserved states this._addProperty(state); //set the default value for the state flag if (state != "disabled" && state != "up") this._stateFlags[state] = false; var inputData = imageSettings[state]; if (inputData) { //if inputData is an object with a tex property, use that //otherwise it is a texture itself if (inputData.tex) _stateData[state] = { tex: inputData.tex }; else _stateData[state] = { tex: inputData }; if (typeof _stateData[state].tex == "string") _stateData[state].tex = Texture.fromImage(_stateData[state].tex); } else { //it's established that over, down, and particularly disabled default to //the up state _stateData[state] = _stateData.up; } //set up the label info for this state if (label) { //if there is actual label data for this state, use that if (inputData && inputData.label) { inputData = inputData.label; var stateLabel = _stateData[state].label = {}; stateLabel.style = inputData.style || labelData.style; stateLabel.x = inputData.x || labelData.x; stateLabel.y = inputData.y || labelData.y; } //otherwise use the default else _stateData[state].label = labelData; } } //ensure that our required states exist if (!_stateData.up) { if (DEBUG && Debug) { Debug.error("Button lacks an up state! This is a serious problem! Input data follows:"); Debug.error(imageSettings); } } if (!_stateData.over) _stateData.over = _stateData.up; if (!_stateData.down) _stateData.down = _stateData.up; if (!_stateData.disabled) _stateData.disabled = _stateData.up; //set up the offset if (imageSettings.offset) { this._offset.x = imageSettings.offset.x; this._offset.y = imageSettings.offset.y; } else { this._offset.x = this._offset.y = 0; } if (imageSettings.scale) { var s = imageSettings.scale || 1; this.back.scale.x = this.back.scale.y = s; } if (label) { this.label = (label.type == "bitmap" && BitmapText) ? new BitmapText(label.text, labelData.style) : new Text(label.text, labelData.style); this.label.setPivotToAlign = true; this.addChild(this.label); } this.back.x = this._offset.x; this.back.y = this._offset.y; this._width = this.back.width; this._height = this.back.height; this.enabled = enabled === undefined ? true : !!enabled; }; // Reference to the prototype var p = extend(Button, Container); /** * An event for when the button is pressed (while enabled). * @static * @property {String} BUTTON_PRESS */ Button.BUTTON_PRESS = "buttonPress"; /** * An event for when the button is moused over (while enabled). * @static * @property {String} BUTTON_OVER */ Button.BUTTON_OVER = "buttonOver"; /** * An event for when the button is moused out (while enabled). * @static * @property {String} BUTTON_OUT */ Button.BUTTON_OUT = "buttonOut"; /* * A list of state names that should not have properties autogenerated. * @private * @static * @property {Array} RESERVED_STATES */ var RESERVED_STATES = ["disabled", "enabled", "up", "over", "down"]; /* * A state priority list to use as the default. * @private * @static * @property {Array} DEFAULT_PRIORITY */ var DEFAULT_PRIORITY = ["disabled", "down", "over", "up"]; /* * A simple function for making a shallow copy of an object. */ function clone(obj) { if (!obj || "object" != typeof obj) return null; var copy = obj.constructor(); for (var attr in obj) { if (obj.hasOwnProperty(attr)) copy[attr] = obj[attr]; } return copy; } /* * The width of the button, based on the width of back. This value is affected by scale. * @property {Number} width */ Object.defineProperty(p, "width", { get: function() { return this._width * this.scale.x; }, set: function(value) { this.scale.x = value / this._width; } }); /* * The height of the button, based on the height of back. This value is affected by scale. * @property {Number} height */ Object.defineProperty(p, "height", { get: function() { return this._height * this.scale.y; }, set: function(value) { this.scale.y = value / this._height; } }); /** * Sets the text of the label. This does nothing if the button was not initialized with a * label. * @method setText * @param {String} text The text to set the label to. */ p.setText = function(text) { if (this.label) { this.label.text = text; //make the text update so we can figure out the size for positioning if (this.label instanceof Text) this.label.updateText(); else this.label.validate(); //position the text var data; for (var i = 0; i < this._statePriority.length; ++i) { if (this._stateFlags[this._statePriority[i]]) { data = this._stateData[this._statePriority[i]]; break; } } if (!data) data = this._stateData.up; data = data.label; if (data.x == "center") { var bW = this.back.width, lW = this.label.width; switch (this._currentLabelStyle.align) { case "center": this.label.position.x = bW * 0.5; break; case "right": this.label.position.x = bw - (bW - lW) * 0.5; break; default: //left or null (defaults to left) this.label.position.x = (bW - lW) * 0.5; break; } } else this.label.position.x = data.x + this._offset.x; if (data.y == "center") { this.label.position.y = (this.back.height - this.label.height) * 0.5; } else this.label.position.y = data.y + this._offset.y; } }; /** * Whether or not the button is enabled. * @property {Boolean} enabled * @default true */ Object.defineProperty(p, "enabled", { get: function() { return !this._stateFlags.disabled; }, set: function(value) { this._stateFlags.disabled = !value; this.buttonMode = value; this.interactive = value; this.off("mousedown", this._onDown); this.off("touchstart", this._onDown); this.off("mouseover", this._onOver); this.off("mouseout", this._onOut); //make sure interaction callbacks are properly set if (value) { this.on("mousedown", this._onDown); this.on("touchstart", this._onDown); this.on("mouseover", this._onOver); this.on("mouseout", this._onOut); } else { this.off("mouseupoutside", this._onUpOutside); this.off("touchendoutside", this._onUpOutside); this.off("mouseup", this._onUp); this.off("touchend", this._onUp); this._stateFlags.down = this._stateFlags.over = false; //also turn off pixi values so that re-enabling button works properly this._over = false; this._touchDown = false; } this._updateState(); } }); /** * Adds a property to the button. Setting the property sets the value in * _stateFlags and calls _updateState(). * @private * @method _addProperty * @param {String} propertyName The property name to add to the button. */ p._addProperty = function(propertyName) { //check to make sure we don't add reserved names if (RESERVED_STATES.indexOf(propertyName) >= 0) return; if (DEBUG && Debug && this[propertyName] !== undefined) { Debug.error("Adding property %s to button is dangerous, as property already exists with that name!", propertyName); } Object.defineProperty(this, propertyName, { get: function() { return this._stateFlags[propertyName]; }, set: function(value) { this._stateFlags[propertyName] = value; this._updateState(); } }); }; /** * Updates back based on the current button state. * @private * @method _updateState * @return {Object} The state data for the active button state, so that subclasses can use the * value picked by this function without needing to calculate it themselves. */ p._updateState = function() { if (!this.back) return; var data; //use the highest priority state for (var i = 0; i < this._statePriority.length; ++i) { if (this._stateFlags[this._statePriority[i]]) { data = this._stateData[this._statePriority[i]]; break; } } //if no state is active, use the up state if (!data) data = this._stateData.up; this.back.texture = data.tex; //if we have a label, update that too if (this.label) { var lData = data.label; var label = this.label; //update the text style if (!this._currentLabelStyle || !doObjectsMatch(this._currentLabelStyle, lData.style)) { label.font = lData.style.font; label.align = lData.style.align; this._currentLabelStyle = lData.style; //make the text update so we can figure out the size for positioning if (label instanceof Text) label.updateText(); else label.validate(); } //position the text if (lData.x == "center") { var bW = this.back.width, lW = label.width; switch (this._currentLabelStyle.align) { case "center": label.position.x = bW * 0.5; break; case "right": label.position.x = bW - (bW - lW) * 0.5; break; default: //left or null (defaults to left) label.position.x = (bW - lW) * 0.5; break; } } else label.position.x = lData.x + this._offset.x; if (lData.y == "center") { label.position.y = (this.back.height - label.height) * 0.5; } else label.position.y = lData.y + this._offset.y; } return data; }; /* * A simple function for comparing the properties of two objects */ function doObjectsMatch(obj1, obj2) { if (obj1 === obj2) return true; for (var key in obj1) { if (obj1[key] != obj2[key]) return false; } return true; } /** * The callback for when the button is moused over. * @private * @method _onOver */ p._onOver = function(event) { this._stateFlags.over = true; this._updateState(); this.emit(Button.BUTTON_OVER, this); }; /** * The callback for when the mouse leaves the button area. * @private * @method _onOut */ p._onOut = function(event) { this._stateFlags.over = false; this._updateState(); this.emit(Button.BUTTON_OUT, this); }; /** * The callback for when the button receives a mouse down event. * @private * @method _onDown */ p._onDown = function(event) { this._stateFlags.down = true; this._updateState(); this.on("mouseupoutside", this._onUpOutside); this.on("touchendoutside", this._onUpOutside); this.on("mouseup", this._onUp); this.on("touchend", this._onUp); }; /** * The callback for when the button for when the mouse/touch is released on the button * - only when the button was held down initially. * @private * @method _onUp */ p._onUp = function(event) { this._stateFlags.down = false; this.off("mouseupoutside", this._onUpOutside); this.off("touchendoutside", this._onUpOutside); this.off("mouseup", this._onUp); this.off("touchend", this._onUp); this._updateState(); //because of the way PIXI handles interaction, it is safer to emit this event outside //the interaction check, in case the user's callback modifies the display list setTimeout(this._emitPress, 0); }; p._emitPress = function() { this.emit(Button.BUTTON_PRESS, this); }; /** * The callback for when the mouse/touch is released outside the button when the button was * held down. * @private * @method _onUpOutside */ p._onUpOutside = function(event) { this._stateFlags.down = false; this.off("mouseupoutside", this._onUpOutside); this.off("touchendoutside", this._onUpOutside); this.off("mouseup", this._onUp); this.off("touchend", this._onUp); this._updateState(); }; /** * Destroys the button. * @public * @method destroy */ p.destroy = function() { this.removeAllListeners(); this.removeChildren(); this.label = null; this.back = null; this._stateData = null; this._stateFlags = null; this._statePriority = null; }; namespace('springroll').Button = Button; namespace('springroll.pixi').Button = Button; }());