From 901621da95ed85f89b4451e1134cf1dee71e070e Mon Sep 17 00:00:00 2001 From: Sebastian Kupke Date: Wed, 9 Oct 2019 15:41:16 +0200 Subject: [PATCH] Added PixiJS UI Text class. --- dist/iwmlib.pixi.js | 721 ++++++++++++++++++++++++- lib/pixi/bundle.js | 2 + lib/pixi/index.html | 1 + lib/pixi/text-transform.html | 162 ++++++ lib/pixi/text.html | 142 +---- lib/pixi/text.js | 714 ++++++++++++++++++++++++ lib/pixi/thumbnails/text-transform.png | Bin 0 -> 29776 bytes package-lock.json | 6 +- package.json | 2 +- 9 files changed, 1611 insertions(+), 139 deletions(-) create mode 100644 lib/pixi/text-transform.html create mode 100644 lib/pixi/text.js create mode 100644 lib/pixi/thumbnails/text-transform.png diff --git a/dist/iwmlib.pixi.js b/dist/iwmlib.pixi.js index e42ce4d..69a9363 100644 --- a/dist/iwmlib.pixi.js +++ b/dist/iwmlib.pixi.js @@ -2277,7 +2277,7 @@ * @see {@link http://pixijs.download/dev/docs/PIXI.Container.html|PIXI.Container} * @see {@link https://www.iwm-tuebingen.de/iwmbrowser/lib/pixi/button.html|DocTest} */ - class Button extends PIXI.Container { + class Button$1 extends PIXI.Container { /** * Creates an instance of a Button. * @@ -2893,7 +2893,7 @@ size = this.opts.minHeight - 2 * this.opts.padding; } - const url = Button.iconIsUrl(icon) ? icon : `../../assets/icons/${icon}.png`; + const url = Button$1.iconIsUrl(icon) ? icon : `../../assets/icons/${icon}.png`; const iconTexture = PIXI.Texture.from(url); const sprite = new PIXI.Sprite(iconTexture); @@ -3171,7 +3171,7 @@ } } - let button = new Button(it); + let button = new Button$1(it); this.container.addChild(button); this.buttons.push(button); @@ -3765,7 +3765,7 @@ //----------------- if (this.opts.button || this.opts.buttonGroup) { if (this.opts.button) { - this._buttons = new Button(Object.assign({ textStyle: this.theme.textStyleSmall }, this.opts.button)); + this._buttons = new Button$1(Object.assign({ textStyle: this.theme.textStyleSmall }, this.opts.button)); } else { this._buttons = new ButtonGroup( Object.assign({ textStyle: this.theme.textStyleSmall }, this.opts.buttonGroup) @@ -16094,6 +16094,716 @@ } } + /* global */ + + /** + * Callback for the button action. + * + * @callback actionCallback + * @param {object} event - The event object. + * @param {Button} button - A reference to the button (also this refers to the button). + */ + + /** + * Callback for the button beforeAction. + * + * @callback beforeActionCallback + * @param {object} event - The event object. + * @param {Button} button - A reference to the button (also this refers to the button). + */ + + /** + * Callback for the button afterAction. + * + * @callback afterActionCallback + * @param {object} event - The event object. + * @param {Button} button - A reference to the button (also this refers to the button). + */ + + /** + * Class that represents a PixiJS Button. + * + * @example + * // Create the button + * const button = new Button({ + * label: 'My Button', + * action: () => console.log('Button was clicked') + * }) + * + * // Add the button to a DisplayObject + * app.scene.addChild(button) + * + * @class + * @extends PIXI.Container + * @see {@link http://pixijs.download/dev/docs/PIXI.Container.html|PIXI.Container} + * @see {@link https://www.iwm-tuebingen.de/iwmbrowser/lib/pixi/button.html|DocTest} + */ + class Text extends PIXI.Container { + /** + * Creates an instance of a Button. + * + * @constructor + * @param {object} [opts] - An options object to specify to style and behaviour of the button. + * @param {number} [opts.id=auto generated] - The id of the button. + * @param {string} [opts.label] - The label of the button. + * @param {number} [opts.x=0] - The x position of the button. Can be also set after creation with button.x = 0. + * @param {number} [opts.y=0] - The y position of the button. Can be also set after creation with button.y = 0. + * @param {string|Theme} [opts.theme=dark] - The theme to use for this button. Possible values are dark, light, red + * or a Theme object. + * @param {number} [opts.minWidth=44] - The minimum width of the button. + * @param {number} [opts.minHeight=44] - The minimum height of the button. + * @param {number} [opts.padding=Theme.padding] - The inner spacing (distance from icon and/or label) to the border. + * @param {string|PIXI.DisplayObject} [opts.icon] - The icon of the button. Can be a predefined one, an URL or an PIXI.DisplayObject. + * @param {string|PIXI.DisplayObject} [opts.iconActive=icon] - The icon of the button when activated. Can be a predefined one, an URL or an PIXI.DisplayObject. + * @param {string} [opts.iconPosition=left] - The position of the icon in relation to the label. Can be left or right. + * @param {number} [opts.iconColor=Theme.iconColor] - The color of the icon (set by the tint property) as a hex value. + * @param {number} [opts.iconColorActive=Theme.iconColorActive] - The color of the icon when activated. + * @param {number} [opts.fill=Theme.fill] - The color of the button background as a hex value. + * @param {number} [opts.fillAlpha=Theme.fillAlpha] - The alpha value of the background. + * @param {number} [opts.fillActive=Theme.fillActive] - The color of the button background when activated. + * @param {number} [opts.fillActiveAlpha=Theme.fillActiveAlpha] - The alpha value of the background when activated. + * @param {number} [opts.stroke=Theme.stroke] - The color of the border as a hex value. + * @param {number} [opts.strokeWidth=Theme.strokeWidth] - The width of the border in pixel. + * @param {number} [opts.strokeAlpha=Theme.strokeAlpha] - The alpha value of the border. + * @param {number} [opts.strokeActive=Theme.strokeActive] - The color of the border when activated. + * @param {number} [opts.strokeActiveWidth=Theme.strokeActiveWidth] - The width of the border in pixel when activated. + * @param {number} [opts.strokeActiveAlpha=Theme.strokeActiveAlpha] - The alpha value of the border when activated. + * @param {object} [opts.textStyle=Theme.textStyle] - A textstyle object for the styling of the label. See PIXI.TextStyle + * for possible options. + * @param {number} [opts.textStyleActive=Theme.textStyleActive] - A textstyle object for the styling of the label when the + * button is activated. See PIXI.TextStyle for possible options. + * @param {string} [opts.style=default] - A shortcut for styling options. Possible values are default, link. + * @param {number} [opts.radius=Theme.radius] - The radius of the four corners of the button (which is a rounded rectangle). + * @param {boolean} [opts.disabled=false] - Is the button disabled? When disabled, the button has a lower alpha value + * and cannot be clicked (interactive is set to false). + * @param {boolean} [opts.active=false] - Is the button initially active? + * @param {actionCallback} [opts.action] - Executed when the button was triggered (by pointerup). + * @param {beforeActionCallback} [opts.beforeAction] - Executed before the main action is triggered. + * @param {afterActionCallback} [opts.afterAction] - Executed after the main action was triggered. + * @param {string} [opts.type=default] - The type of the button. Can be default or checkbox. When the type is + * checkbox, the active state is toggled automatically. + * @param {string} [opts.align=center] - The horizontal position of the label and the icon. Possible values are + * left, center and right. Only affects the style when the minWidth is bigger than the width of the icon and label. + * @param {string} [opts.verticalAlign=middle] - The vertical position of the label and the icon. Possible values are + * top, middle and button. Only affects the style when the minHeight is bigger than the height of the icon and label. + * @param {string|object} [opts.tooltip] - A string for the label of the tooltip or an object to configure the tooltip + * to display. + * @param {string|object} [opts.badge] - A string for the label of the badge or an object to configure the badge to display. + * If the parameter is an object, all badge options can be set plus the following: + * @param {string} [opts.badge.align=right] - The horizontal alignment of the badge. Possible values: left, center, right + * @param {string} [opts.badge.verticalAlign=top] - The vertical alignment of the badge. Possible values: top, middle, bottom + * @param {number} [opts.badge.offsetLeft=0] - The horizontal shift of the badge. + * @param {number} [opts.badge.offsetTop=0] - The vertical shift of the badge. + * @param {boolean} [opts.visible=true] - Is the button initially visible (property visible)? + */ + constructor(opts = {}) { + super(); + + const theme = Theme.fromString(opts.theme); + this.theme = theme; + + this.opts = Object.assign( + {}, + { + id: PIXI.utils.uid(), + label: null, + x: 0, + y: 0, + minWidth: 44, + minHeight: 44, + padding: theme.padding, + icon: undefined, + iconActive: undefined, + iconPosition: 'left', + iconColor: theme.iconColor, + iconColorActive: theme.iconColorActive, + fill: theme.fill, + fillAlpha: theme.fillAlpha, + fillActive: theme.fillActive, + fillActiveAlpha: theme.fillActiveAlpha, + stroke: theme.stroke, + strokeWidth: theme.strokeWidth, + strokeAlpha: theme.strokeAlpha, + strokeActive: theme.strokeActive, + strokeActiveWidth: theme.strokeActiveWidth, + strokeActiveAlpha: theme.strokeActiveAlpha, + textStyle: {}, + textStyleActive: {}, + style: 'default', + radius: theme.radius, + disabled: false, + active: false, + action: null, + beforeAction: null, + afterAction: null, + type: 'default', + align: 'center', + verticalAlign: 'middle', + tooltip: null, + badge: null, + visible: true + }, + opts + ); + + this.id = this.opts.id; + + this.opts.textStyle = Object.assign({}, theme.textStyle, this.opts.textStyle); + this.opts.textStyleActive = Object.assign({}, theme.textStyleActive, this.opts.textStyleActive); + + if (typeof this.opts.icon === 'undefined' && typeof this.opts.iconActive !== 'undefined') { + this.opts.icon = this.opts.iconActive; + } else if (typeof this.opts.icon !== 'undefined' && typeof this.opts.iconActive === 'undefined') { + this.opts.iconActive = this.opts.icon; + } + + if (this.opts.style === 'link') { + Object.assign(this.opts, { + strokeAlpha: 0, + strokeActiveAlpha: 0, + fillAlpha: 0, + fillActiveAlpha: 0 + }); + } + + this._active = null; + this._disabled = null; + + this.__start = { x: null, y: null }; + + this.iconInactive = null; + this.iconActive = null; + this.text = null; + + this.button = null; + this.content = null; + + this.tooltip = null; + this.badge = null; + + this.visible = this.opts.visible; + + // setup + //----------------- + this.setup(); + } + + /** + * Captures an event to inform InteractionMapper about processed events. + * + * @param {event|PIXI.InteractionEvent} event - The PIXI event to capture. + */ + capture(event) { + Events$1.capturedBy(event.data.originalEvent, this); + } + + /** + * Creates children and instantiates everything. + * + * @private + * @return {Button} A reference to the button for chaining. + */ + setup() { + // Button + //----------------- + let button = new PIXI.Graphics(); + this.button = button; + this.addChild(button); + + // Content + //----------------- + let content = new PIXI.Container(); + this.content = content; + this.addChild(content); + + // Text + //----------------- + if (this.opts.label) { + this.text = new PIXI.Text(this.opts.label, this.opts.textStyle); + } + + // Icon + //----------------- + if (this.opts.icon) { + this.iconInactive = this.loadIcon(this.opts.icon, this.opts.iconColor); + } + + if (this.opts.iconActive) { + this.iconActive = this.loadIcon(this.opts.iconActive, this.opts.iconColorActive); + } + + // interaction + //----------------- + this.button.on('pointerover', e => { + this.capture(e); + TweenLite.to([this.button, this.content], this.theme.fast, { + alpha: 0.83, + overwrite: 'none' + }); + }); + + this.button.on('pointermove', e => { + this.capture(e); + }); + + this.button.on('pointerout', this.onEnd.bind(this)); + this.button.on('pointercancel', this.onEnd.bind(this)); + this.button.on('pointerupoutside', this.onEnd.bind(this)); + this.button.on('pointertap', this.onEnd.bind(this)); + this.button.on('scroll', this.onEnd.bind(this)); + + // eslint-disable-next-line no-unused-vars + this.button.on('pointerdown', e => { + //this.capture(e) + this.__start.x = e.data.global.x; + this.__start.y = e.data.global.y; + TweenLite.to([this.button, this.content], this.theme.fast, { + alpha: 0.7, + overwrite: 'none' + }); + }); + + this.button.on('pointerup', e => { + this.capture(e); + + const distance = Points.distance(e.data.global, this.__start); + + if (distance < 5) { + if (this.opts.beforeAction) { + this.opts.beforeAction.call(this, e, this); + } + + if (this.opts.action) { + this.opts.action.call(this, e, this); + } + + TweenLite.to([this.button, this.content], this.theme.fast, { + alpha: 0.83, + overwrite: 'none' + }); + + if (this.opts.type === 'checkbox') { + this.active = !this.active; + } + + if (this.opts.afterAction) { + this.opts.afterAction.call(this, e, this); + } + } + }); + + // disabled + //----------------- + this.disabled = this.opts.disabled; + + // active + //----------------- + this.active = this.opts.active; // calls .layout() + + // tooltip + //----------------- + if (this.opts.tooltip) { + if (typeof this.opts.tooltip === 'string') { + this.tooltip = new Tooltip({ + object: this, + content: this.opts.tooltip + }); + } else { + this.opts.tooltip = Object.assign({}, { object: this }, this.opts.tooltip); + this.tooltip = new Tooltip(this.opts.tooltip); + } + } + + // badge + //----------------- + if (this.opts.badge) { + let opts = Object.assign( + {}, + { + align: 'right', + verticalAlign: 'top', + offsetLeft: 0, + offsetTop: 0 + } + ); + if (typeof this.opts.badge === 'string') { + opts = Object.assign(opts, { content: this.opts.badge }); + } else { + opts = Object.assign(opts, this.opts.badge); + } + + const badge = new Badge(opts); + this.addChild(badge); + this.badge = badge; + } + + this.layout(); + + // set position + //----------------- + this.position.set(this.opts.x, this.opts.y); + + return this + } + + /** + * Should be called to refresh the layout of the button. Can be used after resizing. + * + * @return {Button} A reference to the button for chaining. + */ + layout() { + // Clear content + //----------------- + this.removeChild(this.content); + this.content = new PIXI.Container(); + this.addChild(this.content); + + // Set the icon + //----------------- + let icon = null; + + if (!this.active && this.iconInactive) { + icon = this.iconInactive; + } else if (this.active && this.iconActive) { + icon = this.iconActive; + } + + // Set the text + //----------------- + if (this.text) { + this.text.position.set(0, 0); + } + + // Width and Height + //----------------- + let width = 0; + if (icon && this.text) { + width = icon.width + this.text.width + 3 * this.opts.padding; + } else if (icon) { + width = icon.width + 2 * this.opts.padding; + } else if (this.text) { + width = this.text.width + 2 * this.opts.padding; + } + + if (width < this.opts.minWidth) { + width = this.opts.minWidth; + } + + let height = 0; + if (icon) { + height = icon.height + 2 * this.opts.padding; + } else if (this.text) { + height = this.text.height + 2 * this.opts.padding; + } + + if (height < this.opts.minHeight) { + height = this.opts.minHeight; + } + + this._width = width; + this._height = height; + + // Position icon and text + //----------------- + if (icon && this.text) { + if (this.opts.iconPosition === 'right') { + icon.x = this.text.width + this.opts.padding; + } else { + this.text.x = icon.width + this.opts.padding; + } + this.content.addChild(icon, this.text); + } else if (icon) { + this.content.addChild(icon); + } else if (this.text) { + this.content.addChild(this.text); + } + + this.layoutInnerContent(); + this.layoutContent(); + + this.icon = icon; + + // badge + //-------------------- + if (this.badge) { + this.removeChild(this.badge); + const width = this._width; + const height = this._height; + this.addChild(this.badge); + + const badge = this.badge; + + switch (badge.opts.align) { + case 'left': + badge.x = -badge.width / 2 + badge.opts.offsetLeft; + break + case 'center': + badge.x = width / 2 - badge.width / 2 + badge.opts.offsetLeft; + break + case 'right': + badge.x = width - badge.width / 2 + badge.opts.offsetLeft; + } + + switch (badge.opts.verticalAlign) { + case 'top': + badge.y = -badge.height / 2 + badge.opts.offsetTop; + break + case 'middle': + badge.y = height / 2 - badge.height / 2 + badge.opts.offsetTop; + break + case 'bottom': + badge.y = height - badge.height / 2 + badge.opts.offsetTop; + } + } + + // draw + //----------------- + this.draw(); + + return this + } + + /** + * Calculates the positions of the content children (icon and/or text). + * + * @private + * @return {Button} A reference to the button for chaining. + */ + layoutInnerContent() { + for (let child of this.content.children) { + switch (this.opts.verticalAlign) { + case 'top': + child.y = 0; + break + case 'middle': + child.y = this.content.height / 2 - child.height / 2; + break + case 'bottom': + child.y = this.content.height - child.height; + break + } + } + + return this + } + + /** + * Sets the horizontal and vertical position of the content. + * Uses the option keys "align" and "verticalAlign". + * + * @private + * @return {Button} A reference to the button for chaining. + */ + layoutContent() { + switch (this.opts.align) { + case 'left': + this.content.x = this.opts.padding; + break + case 'center': + this.content.x = (this._width - this.content.width) / 2; + break + case 'right': + this.content.x = this._width - this.opts.padding - this.content.width; + break + } + + switch (this.opts.verticalAlign) { + case 'top': + this.content.y = this.opts.padding; + break + case 'middle': + this.content.y = (this._height - this.content.height) / 2; + break + case 'bottom': + this.content.y = this._height - this.opts.padding - this.content.height; + break + } + + return this + } + + /** + * Draws the canvas. + * + * @private + * @return {Button} A reference to the button for chaining. + */ + draw() { + this.button.clear(); + if (this.active) { + this.button.lineStyle(this.opts.strokeActiveWidth, this.opts.strokeActive, this.opts.strokeActiveAlpha); + this.button.beginFill(this.opts.fillActive, this.opts.fillActiveAlpha); + } else { + this.button.lineStyle(this.opts.strokeWidth, this.opts.stroke, this.opts.strokeAlpha); + this.button.beginFill(this.opts.fill, this.opts.fillAlpha); + } + this.button.drawRoundedRect(0, 0, this._width, this._height, this.opts.radius); + this.button.endFill(); + + return this + } + + /** + * Gets or sets the active state. + * + * @member {boolean} + */ + get active() { + return this._active + } + set active(value) { + this._active = value; + + if (this._active) { + if (this.text) { + this.text.style = this.opts.textStyleActive; + } + } else { + if (this.text) { + this.text.style = this.opts.textStyle; + } + } + + this.layout(); + } + + /** + * Gets or sets the disabled state. When disabled, the button cannot be clicked. + * + * @member {boolean} + */ + get disabled() { + return this._disabled + } + set disabled(value) { + this._disabled = value; + + if (this._disabled) { + this.button.interactive = false; + this.button.buttonMode = false; + this.button.alpha = 0.5; + if (this.icon) { + this.icon.alpha = 0.5; + } + if (this.text) { + this.text.alpha = 0.5; + } + } else { + this.button.interactive = true; + this.button.buttonMode = true; + this.button.alpha = 1; + if (this.icon) { + this.icon.alpha = 1; + } + if (this.text) { + this.text.alpha = 1; + } + } + } + + /** + * Shows the button (sets his alpha values to 1). + * + * @return {Button} A reference to the button for chaining. + */ + show() { + this.opts.strokeAlpha = 1; + this.opts.strokeActiveAlpha = 1; + this.opts.fillAlpha = 1; + this.opts.fillActiveAlpha = 1; + + this.layout(); + + return this + } + + /** + * Hides the button (sets his alpha values to 0). + * + * @return {Button} A reference to the button for chaining. + */ + hide() { + this.opts.strokeAlpha = 0.0; + this.opts.strokeActiveAlpha = 0.0; + this.opts.fillAlpha = 0.0000000001; // WORKAROUND: See https://github.com/pixijs/pixi.js/wiki/v5-Migration-Guide#graphics-interaction + this.opts.fillActiveAlpha = 0.0000000001; + + this.layout(); + + return this + } + + /** + * Loads an icon + * + * @private + * @param {string|PIXI.DisplayObject} icon - The icon to load. + * @param {number} color - The color of the icon (if not an PIXI.DisplayObject). + * @return {PIXI.DisplayObject} Return the icon as an PIXI.DisplayObject. + */ + loadIcon(icon, color) { + let displayObject = null; + + if (icon instanceof PIXI.DisplayObject) { + displayObject = icon; + } else { + let size = 17; + if (this.text) { + size = this.text.height; + } else if (this.opts.minHeight) { + size = this.opts.minHeight - 2 * this.opts.padding; + } + + const url = Button.iconIsUrl(icon) ? icon : `../../assets/icons/${icon}.png`; + const iconTexture = PIXI.Texture.from(url); + + const sprite = new PIXI.Sprite(iconTexture); + sprite.tint = color; + sprite.width = size; + sprite.height = size; + + displayObject = sprite; + } + + return displayObject + } + + /** + * Tests if an icon string is an url. + * + * @private + * @static + * @param {string} url - The url to test. + * @return {boolean} true if the url is an url to an image. + */ + static iconIsUrl(url) { + return /\.(png|svg|gif|jpg|jpeg|tif|tiff)$/i.test(url) + } + + /** + * Gets or sets the color of the current icon (no matter how the status is). Changing the color, changes + * the tint property of the icon sprite. + * + * @member {number} + */ + get iconColor() { + return this.icon ? this.icon.tint : null + } + set iconColor(value) { + if (this.icon) { + this.icon.tint = value; + } + } + + onEnd(event) { + this.capture(event); + TweenLite.to([this.button, this.content], this.theme.fast, { + alpha: 1, + overwrite: 'none' + }); + } + } + /* Needed to ensure that rollup.js includes class definitions and the classes are visible inside doctests. */ @@ -16111,7 +16821,7 @@ window.Timeline = Timeline; window.AppTest = AppTest; window.Theme = Theme; - window.Button = Button; + window.Button = Button$1; window.ButtonGroup = ButtonGroup; window.Scrollview = Scrollview; window.Slider = Slider; @@ -16128,5 +16838,6 @@ window.List = List; window.LabeledGraphics = LabeledGraphics; window.FontInfo = FontInfo; + window.Text = Text; }()); diff --git a/lib/pixi/bundle.js b/lib/pixi/bundle.js index 49d51aa..15856b8 100755 --- a/lib/pixi/bundle.js +++ b/lib/pixi/bundle.js @@ -24,6 +24,7 @@ import Badge from './badge.js' import Progress from './progress.js' import List from './list.js' import { LabeledGraphics, FontInfo } from './labeledgraphics.js' +import Text from './text.js' /* Needed to ensure that rollup.js includes class definitions and the classes are visible inside doctests. */ @@ -58,3 +59,4 @@ window.Progress = Progress window.List = List window.LabeledGraphics = LabeledGraphics window.FontInfo = FontInfo +window.Text = Text diff --git a/lib/pixi/index.html b/lib/pixi/index.html index 7a106e5..bbce2ea 100644 --- a/lib/pixi/index.html +++ b/lib/pixi/index.html @@ -51,6 +51,7 @@ const index = new Index(itemTemplate, [ ['Flip Effect', 'flipeffect.html'], ['Blur Filter', 'blurfilter.html'], ['Text', 'text.html'], + ['Text Transform', 'text-transform.html'], ['Scrollview', 'scrollview.html'], ['Stylus', 'stylus.html'] ], diff --git a/lib/pixi/text-transform.html b/lib/pixi/text-transform.html new file mode 100644 index 0000000..60faa13 --- /dev/null +++ b/lib/pixi/text-transform.html @@ -0,0 +1,162 @@ + + + + + + PIXI Text + + + + + + + + + + + + + +

Text

+

+ The tooltip or infotip or a hint is a common graphical user interface element. It is used in conjunction with a cursor, usually a pointer. The user hovers the pointer over an item, without clicking it, and a tooltip may appear—a small "hover box" with information about the item being hovered over.[1][2] Tooltips do not usually appear on mobile operating systems, because there is no cursor (though tooltips may be displayed when using a mouse). +

+

Let's look at some tooltip examples:


+ +

+ What you should see: Ten colored circles with different tooltips. +

+ + diff --git a/lib/pixi/text.html b/lib/pixi/text.html index 60faa13..8fc6e8d 100644 --- a/lib/pixi/text.html +++ b/lib/pixi/text.html @@ -13,150 +13,32 @@ - -

Text

- The tooltip or infotip or a hint is a common graphical user interface element. It is used in conjunction with a cursor, usually a pointer. The user hovers the pointer over an item, without clicking it, and a tooltip may appear—a small "hover box" with information about the item being hovered over.[1][2] Tooltips do not usually appear on mobile operating systems, because there is no cursor (though tooltips may be displayed when using a mouse). + The Button class defines a clickable/touchable button. Use custom button styles for actions in forms, dialogs, + and more with support for multiple sizes, states, and more. Buttons will appear pressed when active. Make + buttons look inactive by setting the disabled state to true. To allow changing the state between active/inactive, set + the button type to "checkbox".

-

Let's look at some tooltip examples:


+

JavaScript API

+

Let's look at some text examples:


- What you should see: Ten colored circles with different tooltips. + What you should see: Many texts with very different styling and behaviour.

+ \ No newline at end of file diff --git a/lib/pixi/text.js b/lib/pixi/text.js new file mode 100644 index 0000000..f8725bf --- /dev/null +++ b/lib/pixi/text.js @@ -0,0 +1,714 @@ +/* global */ +import Theme from './theme.js' +import Tooltip from './tooltip.js' +import Badge from './badge.js' +import Events from '../events.js' +import { Points } from '../utils.js' + +/** + * Callback for the button action. + * + * @callback actionCallback + * @param {object} event - The event object. + * @param {Button} button - A reference to the button (also this refers to the button). + */ + +/** + * Callback for the button beforeAction. + * + * @callback beforeActionCallback + * @param {object} event - The event object. + * @param {Button} button - A reference to the button (also this refers to the button). + */ + +/** + * Callback for the button afterAction. + * + * @callback afterActionCallback + * @param {object} event - The event object. + * @param {Button} button - A reference to the button (also this refers to the button). + */ + +/** + * Class that represents a PixiJS Button. + * + * @example + * // Create the button + * const button = new Button({ + * label: 'My Button', + * action: () => console.log('Button was clicked') + * }) + * + * // Add the button to a DisplayObject + * app.scene.addChild(button) + * + * @class + * @extends PIXI.Container + * @see {@link http://pixijs.download/dev/docs/PIXI.Container.html|PIXI.Container} + * @see {@link https://www.iwm-tuebingen.de/iwmbrowser/lib/pixi/button.html|DocTest} + */ +export default class Text extends PIXI.Container { + /** + * Creates an instance of a Button. + * + * @constructor + * @param {object} [opts] - An options object to specify to style and behaviour of the button. + * @param {number} [opts.id=auto generated] - The id of the button. + * @param {string} [opts.label] - The label of the button. + * @param {number} [opts.x=0] - The x position of the button. Can be also set after creation with button.x = 0. + * @param {number} [opts.y=0] - The y position of the button. Can be also set after creation with button.y = 0. + * @param {string|Theme} [opts.theme=dark] - The theme to use for this button. Possible values are dark, light, red + * or a Theme object. + * @param {number} [opts.minWidth=44] - The minimum width of the button. + * @param {number} [opts.minHeight=44] - The minimum height of the button. + * @param {number} [opts.padding=Theme.padding] - The inner spacing (distance from icon and/or label) to the border. + * @param {string|PIXI.DisplayObject} [opts.icon] - The icon of the button. Can be a predefined one, an URL or an PIXI.DisplayObject. + * @param {string|PIXI.DisplayObject} [opts.iconActive=icon] - The icon of the button when activated. Can be a predefined one, an URL or an PIXI.DisplayObject. + * @param {string} [opts.iconPosition=left] - The position of the icon in relation to the label. Can be left or right. + * @param {number} [opts.iconColor=Theme.iconColor] - The color of the icon (set by the tint property) as a hex value. + * @param {number} [opts.iconColorActive=Theme.iconColorActive] - The color of the icon when activated. + * @param {number} [opts.fill=Theme.fill] - The color of the button background as a hex value. + * @param {number} [opts.fillAlpha=Theme.fillAlpha] - The alpha value of the background. + * @param {number} [opts.fillActive=Theme.fillActive] - The color of the button background when activated. + * @param {number} [opts.fillActiveAlpha=Theme.fillActiveAlpha] - The alpha value of the background when activated. + * @param {number} [opts.stroke=Theme.stroke] - The color of the border as a hex value. + * @param {number} [opts.strokeWidth=Theme.strokeWidth] - The width of the border in pixel. + * @param {number} [opts.strokeAlpha=Theme.strokeAlpha] - The alpha value of the border. + * @param {number} [opts.strokeActive=Theme.strokeActive] - The color of the border when activated. + * @param {number} [opts.strokeActiveWidth=Theme.strokeActiveWidth] - The width of the border in pixel when activated. + * @param {number} [opts.strokeActiveAlpha=Theme.strokeActiveAlpha] - The alpha value of the border when activated. + * @param {object} [opts.textStyle=Theme.textStyle] - A textstyle object for the styling of the label. See PIXI.TextStyle + * for possible options. + * @param {number} [opts.textStyleActive=Theme.textStyleActive] - A textstyle object for the styling of the label when the + * button is activated. See PIXI.TextStyle for possible options. + * @param {string} [opts.style=default] - A shortcut for styling options. Possible values are default, link. + * @param {number} [opts.radius=Theme.radius] - The radius of the four corners of the button (which is a rounded rectangle). + * @param {boolean} [opts.disabled=false] - Is the button disabled? When disabled, the button has a lower alpha value + * and cannot be clicked (interactive is set to false). + * @param {boolean} [opts.active=false] - Is the button initially active? + * @param {actionCallback} [opts.action] - Executed when the button was triggered (by pointerup). + * @param {beforeActionCallback} [opts.beforeAction] - Executed before the main action is triggered. + * @param {afterActionCallback} [opts.afterAction] - Executed after the main action was triggered. + * @param {string} [opts.type=default] - The type of the button. Can be default or checkbox. When the type is + * checkbox, the active state is toggled automatically. + * @param {string} [opts.align=center] - The horizontal position of the label and the icon. Possible values are + * left, center and right. Only affects the style when the minWidth is bigger than the width of the icon and label. + * @param {string} [opts.verticalAlign=middle] - The vertical position of the label and the icon. Possible values are + * top, middle and button. Only affects the style when the minHeight is bigger than the height of the icon and label. + * @param {string|object} [opts.tooltip] - A string for the label of the tooltip or an object to configure the tooltip + * to display. + * @param {string|object} [opts.badge] - A string for the label of the badge or an object to configure the badge to display. + * If the parameter is an object, all badge options can be set plus the following: + * @param {string} [opts.badge.align=right] - The horizontal alignment of the badge. Possible values: left, center, right + * @param {string} [opts.badge.verticalAlign=top] - The vertical alignment of the badge. Possible values: top, middle, bottom + * @param {number} [opts.badge.offsetLeft=0] - The horizontal shift of the badge. + * @param {number} [opts.badge.offsetTop=0] - The vertical shift of the badge. + * @param {boolean} [opts.visible=true] - Is the button initially visible (property visible)? + */ + constructor(opts = {}) { + super() + + const theme = Theme.fromString(opts.theme) + this.theme = theme + + this.opts = Object.assign( + {}, + { + id: PIXI.utils.uid(), + label: null, + x: 0, + y: 0, + minWidth: 44, + minHeight: 44, + padding: theme.padding, + icon: undefined, + iconActive: undefined, + iconPosition: 'left', + iconColor: theme.iconColor, + iconColorActive: theme.iconColorActive, + fill: theme.fill, + fillAlpha: theme.fillAlpha, + fillActive: theme.fillActive, + fillActiveAlpha: theme.fillActiveAlpha, + stroke: theme.stroke, + strokeWidth: theme.strokeWidth, + strokeAlpha: theme.strokeAlpha, + strokeActive: theme.strokeActive, + strokeActiveWidth: theme.strokeActiveWidth, + strokeActiveAlpha: theme.strokeActiveAlpha, + textStyle: {}, + textStyleActive: {}, + style: 'default', + radius: theme.radius, + disabled: false, + active: false, + action: null, + beforeAction: null, + afterAction: null, + type: 'default', + align: 'center', + verticalAlign: 'middle', + tooltip: null, + badge: null, + visible: true + }, + opts + ) + + this.id = this.opts.id + + this.opts.textStyle = Object.assign({}, theme.textStyle, this.opts.textStyle) + this.opts.textStyleActive = Object.assign({}, theme.textStyleActive, this.opts.textStyleActive) + + if (typeof this.opts.icon === 'undefined' && typeof this.opts.iconActive !== 'undefined') { + this.opts.icon = this.opts.iconActive + } else if (typeof this.opts.icon !== 'undefined' && typeof this.opts.iconActive === 'undefined') { + this.opts.iconActive = this.opts.icon + } + + if (this.opts.style === 'link') { + Object.assign(this.opts, { + strokeAlpha: 0, + strokeActiveAlpha: 0, + fillAlpha: 0, + fillActiveAlpha: 0 + }) + } + + this._active = null + this._disabled = null + + this.__start = { x: null, y: null } + + this.iconInactive = null + this.iconActive = null + this.text = null + + this.button = null + this.content = null + + this.tooltip = null + this.badge = null + + this.visible = this.opts.visible + + // setup + //----------------- + this.setup() + } + + /** + * Captures an event to inform InteractionMapper about processed events. + * + * @param {event|PIXI.InteractionEvent} event - The PIXI event to capture. + */ + capture(event) { + Events.capturedBy(event.data.originalEvent, this) + } + + /** + * Creates children and instantiates everything. + * + * @private + * @return {Button} A reference to the button for chaining. + */ + setup() { + // Button + //----------------- + let button = new PIXI.Graphics() + this.button = button + this.addChild(button) + + // Content + //----------------- + let content = new PIXI.Container() + this.content = content + this.addChild(content) + + // Text + //----------------- + if (this.opts.label) { + this.text = new PIXI.Text(this.opts.label, this.opts.textStyle) + } + + // Icon + //----------------- + if (this.opts.icon) { + this.iconInactive = this.loadIcon(this.opts.icon, this.opts.iconColor) + } + + if (this.opts.iconActive) { + this.iconActive = this.loadIcon(this.opts.iconActive, this.opts.iconColorActive) + } + + // interaction + //----------------- + this.button.on('pointerover', e => { + this.capture(e) + TweenLite.to([this.button, this.content], this.theme.fast, { + alpha: 0.83, + overwrite: 'none' + }) + }) + + this.button.on('pointermove', e => { + this.capture(e) + }) + + this.button.on('pointerout', this.onEnd.bind(this)) + this.button.on('pointercancel', this.onEnd.bind(this)) + this.button.on('pointerupoutside', this.onEnd.bind(this)) + this.button.on('pointertap', this.onEnd.bind(this)) + this.button.on('scroll', this.onEnd.bind(this)) + + // eslint-disable-next-line no-unused-vars + this.button.on('pointerdown', e => { + //this.capture(e) + this.__start.x = e.data.global.x + this.__start.y = e.data.global.y + TweenLite.to([this.button, this.content], this.theme.fast, { + alpha: 0.7, + overwrite: 'none' + }) + }) + + this.button.on('pointerup', e => { + this.capture(e) + + const distance = Points.distance(e.data.global, this.__start) + + if (distance < 5) { + if (this.opts.beforeAction) { + this.opts.beforeAction.call(this, e, this) + } + + if (this.opts.action) { + this.opts.action.call(this, e, this) + } + + TweenLite.to([this.button, this.content], this.theme.fast, { + alpha: 0.83, + overwrite: 'none' + }) + + if (this.opts.type === 'checkbox') { + this.active = !this.active + } + + if (this.opts.afterAction) { + this.opts.afterAction.call(this, e, this) + } + } + }) + + // disabled + //----------------- + this.disabled = this.opts.disabled + + // active + //----------------- + this.active = this.opts.active // calls .layout() + + // tooltip + //----------------- + if (this.opts.tooltip) { + if (typeof this.opts.tooltip === 'string') { + this.tooltip = new Tooltip({ + object: this, + content: this.opts.tooltip + }) + } else { + this.opts.tooltip = Object.assign({}, { object: this }, this.opts.tooltip) + this.tooltip = new Tooltip(this.opts.tooltip) + } + } + + // badge + //----------------- + if (this.opts.badge) { + let opts = Object.assign( + {}, + { + align: 'right', + verticalAlign: 'top', + offsetLeft: 0, + offsetTop: 0 + } + ) + if (typeof this.opts.badge === 'string') { + opts = Object.assign(opts, { content: this.opts.badge }) + } else { + opts = Object.assign(opts, this.opts.badge) + } + + const badge = new Badge(opts) + this.addChild(badge) + this.badge = badge + } + + this.layout() + + // set position + //----------------- + this.position.set(this.opts.x, this.opts.y) + + return this + } + + /** + * Should be called to refresh the layout of the button. Can be used after resizing. + * + * @return {Button} A reference to the button for chaining. + */ + layout() { + // Clear content + //----------------- + this.removeChild(this.content) + this.content = new PIXI.Container() + this.addChild(this.content) + + // Set the icon + //----------------- + let icon = null + + if (!this.active && this.iconInactive) { + icon = this.iconInactive + } else if (this.active && this.iconActive) { + icon = this.iconActive + } + + // Set the text + //----------------- + if (this.text) { + this.text.position.set(0, 0) + } + + // Width and Height + //----------------- + let width = 0 + if (icon && this.text) { + width = icon.width + this.text.width + 3 * this.opts.padding + } else if (icon) { + width = icon.width + 2 * this.opts.padding + } else if (this.text) { + width = this.text.width + 2 * this.opts.padding + } + + if (width < this.opts.minWidth) { + width = this.opts.minWidth + } + + let height = 0 + if (icon) { + height = icon.height + 2 * this.opts.padding + } else if (this.text) { + height = this.text.height + 2 * this.opts.padding + } + + if (height < this.opts.minHeight) { + height = this.opts.minHeight + } + + this._width = width + this._height = height + + // Position icon and text + //----------------- + if (icon && this.text) { + if (this.opts.iconPosition === 'right') { + icon.x = this.text.width + this.opts.padding + } else { + this.text.x = icon.width + this.opts.padding + } + this.content.addChild(icon, this.text) + } else if (icon) { + this.content.addChild(icon) + } else if (this.text) { + this.content.addChild(this.text) + } + + this.layoutInnerContent() + this.layoutContent() + + this.icon = icon + + // badge + //-------------------- + if (this.badge) { + this.removeChild(this.badge) + const width = this._width + const height = this._height + this.addChild(this.badge) + + const badge = this.badge + + switch (badge.opts.align) { + case 'left': + badge.x = -badge.width / 2 + badge.opts.offsetLeft + break + case 'center': + badge.x = width / 2 - badge.width / 2 + badge.opts.offsetLeft + break + case 'right': + badge.x = width - badge.width / 2 + badge.opts.offsetLeft + } + + switch (badge.opts.verticalAlign) { + case 'top': + badge.y = -badge.height / 2 + badge.opts.offsetTop + break + case 'middle': + badge.y = height / 2 - badge.height / 2 + badge.opts.offsetTop + break + case 'bottom': + badge.y = height - badge.height / 2 + badge.opts.offsetTop + } + } + + // draw + //----------------- + this.draw() + + return this + } + + /** + * Calculates the positions of the content children (icon and/or text). + * + * @private + * @return {Button} A reference to the button for chaining. + */ + layoutInnerContent() { + for (let child of this.content.children) { + switch (this.opts.verticalAlign) { + case 'top': + child.y = 0 + break + case 'middle': + child.y = this.content.height / 2 - child.height / 2 + break + case 'bottom': + child.y = this.content.height - child.height + break + } + } + + return this + } + + /** + * Sets the horizontal and vertical position of the content. + * Uses the option keys "align" and "verticalAlign". + * + * @private + * @return {Button} A reference to the button for chaining. + */ + layoutContent() { + switch (this.opts.align) { + case 'left': + this.content.x = this.opts.padding + break + case 'center': + this.content.x = (this._width - this.content.width) / 2 + break + case 'right': + this.content.x = this._width - this.opts.padding - this.content.width + break + } + + switch (this.opts.verticalAlign) { + case 'top': + this.content.y = this.opts.padding + break + case 'middle': + this.content.y = (this._height - this.content.height) / 2 + break + case 'bottom': + this.content.y = this._height - this.opts.padding - this.content.height + break + } + + return this + } + + /** + * Draws the canvas. + * + * @private + * @return {Button} A reference to the button for chaining. + */ + draw() { + this.button.clear() + if (this.active) { + this.button.lineStyle(this.opts.strokeActiveWidth, this.opts.strokeActive, this.opts.strokeActiveAlpha) + this.button.beginFill(this.opts.fillActive, this.opts.fillActiveAlpha) + } else { + this.button.lineStyle(this.opts.strokeWidth, this.opts.stroke, this.opts.strokeAlpha) + this.button.beginFill(this.opts.fill, this.opts.fillAlpha) + } + this.button.drawRoundedRect(0, 0, this._width, this._height, this.opts.radius) + this.button.endFill() + + return this + } + + /** + * Gets or sets the active state. + * + * @member {boolean} + */ + get active() { + return this._active + } + set active(value) { + this._active = value + + if (this._active) { + if (this.text) { + this.text.style = this.opts.textStyleActive + } + } else { + if (this.text) { + this.text.style = this.opts.textStyle + } + } + + this.layout() + } + + /** + * Gets or sets the disabled state. When disabled, the button cannot be clicked. + * + * @member {boolean} + */ + get disabled() { + return this._disabled + } + set disabled(value) { + this._disabled = value + + if (this._disabled) { + this.button.interactive = false + this.button.buttonMode = false + this.button.alpha = 0.5 + if (this.icon) { + this.icon.alpha = 0.5 + } + if (this.text) { + this.text.alpha = 0.5 + } + } else { + this.button.interactive = true + this.button.buttonMode = true + this.button.alpha = 1 + if (this.icon) { + this.icon.alpha = 1 + } + if (this.text) { + this.text.alpha = 1 + } + } + } + + /** + * Shows the button (sets his alpha values to 1). + * + * @return {Button} A reference to the button for chaining. + */ + show() { + this.opts.strokeAlpha = 1 + this.opts.strokeActiveAlpha = 1 + this.opts.fillAlpha = 1 + this.opts.fillActiveAlpha = 1 + + this.layout() + + return this + } + + /** + * Hides the button (sets his alpha values to 0). + * + * @return {Button} A reference to the button for chaining. + */ + hide() { + this.opts.strokeAlpha = 0.0 + this.opts.strokeActiveAlpha = 0.0 + this.opts.fillAlpha = 0.0000000001 // WORKAROUND: See https://github.com/pixijs/pixi.js/wiki/v5-Migration-Guide#graphics-interaction + this.opts.fillActiveAlpha = 0.0000000001 + + this.layout() + + return this + } + + /** + * Loads an icon + * + * @private + * @param {string|PIXI.DisplayObject} icon - The icon to load. + * @param {number} color - The color of the icon (if not an PIXI.DisplayObject). + * @return {PIXI.DisplayObject} Return the icon as an PIXI.DisplayObject. + */ + loadIcon(icon, color) { + let displayObject = null + + if (icon instanceof PIXI.DisplayObject) { + displayObject = icon + } else { + let size = 17 + if (this.text) { + size = this.text.height + } else if (this.opts.minHeight) { + size = this.opts.minHeight - 2 * this.opts.padding + } + + const url = Button.iconIsUrl(icon) ? icon : `../../assets/icons/${icon}.png` + const iconTexture = PIXI.Texture.from(url) + + const sprite = new PIXI.Sprite(iconTexture) + sprite.tint = color + sprite.width = size + sprite.height = size + + displayObject = sprite + } + + return displayObject + } + + /** + * Tests if an icon string is an url. + * + * @private + * @static + * @param {string} url - The url to test. + * @return {boolean} true if the url is an url to an image. + */ + static iconIsUrl(url) { + return /\.(png|svg|gif|jpg|jpeg|tif|tiff)$/i.test(url) + } + + /** + * Gets or sets the color of the current icon (no matter how the status is). Changing the color, changes + * the tint property of the icon sprite. + * + * @member {number} + */ + get iconColor() { + return this.icon ? this.icon.tint : null + } + set iconColor(value) { + if (this.icon) { + this.icon.tint = value + } + } + + onEnd(event) { + this.capture(event) + TweenLite.to([this.button, this.content], this.theme.fast, { + alpha: 1, + overwrite: 'none' + }) + } +} diff --git a/lib/pixi/thumbnails/text-transform.png b/lib/pixi/thumbnails/text-transform.png new file mode 100644 index 0000000000000000000000000000000000000000..87e4d2ce538b2ad5ec1c6df7b5017a54e0e19a22 GIT binary patch literal 29776 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 literal 0 HcmV?d00001 diff --git a/package-lock.json b/package-lock.json index 9fcf615..baf037b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6122,9 +6122,9 @@ "integrity": "sha512-bvZgRGjdEJWHGNLw1v625wmeJLn/V0vu6nP4eGWsRVvGR24ipBDaMqpaO0p9yhjH7kt8yEGdKsoAkOVvD6bp4Q==" }, "pixi-viewport": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/pixi-viewport/-/pixi-viewport-4.2.3.tgz", - "integrity": "sha512-a/8ZO/QIb1Tg+u3hCjuY/pJh9Q1R514xnFKA3fD7nwwCIPA1ljz0VczOwapNTpEWRUQEAHR3rT9p2iOJYC4+Sg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/pixi-viewport/-/pixi-viewport-4.3.1.tgz", + "integrity": "sha512-7O5s2Lxd2j8d+efBl5refFSd+kC+T3P0GCcv+GaRmX6zuEM2b+5AFrDixzf/w8mJcYplCL8H6dFT4t3ruoZLRQ==", "requires": { "penner": "^0.1.3" } diff --git a/package.json b/package.json index c688dc1..e264fda 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "pixi-filters": "^3.0.3", "pixi-particles": "^4.1.1", "pixi-projection": "^0.3.5", - "pixi-viewport": "^4.2.3", + "pixi-viewport": "^4.3.1", "pixi.js": "^5.1.5", "propagating-hammerjs": "^1.4.7" }