/** * Defines usefull default text styles. */ export class FontInfo { static get small() { return app.theme.textStyleSmall } static get normal() { return app.theme.textStyle } static get centered() { return Object.assign({}, app.theme.textStyle, { align: 'center' }) } } /** * Static methods to support hyphenation of lines. * * @class Hypenate */ export class Hypenate { static splitPart(part) { let parts = part.split('-') if (parts.length == 1) return [part] let result = [] let last = parts.pop() for (let p of parts) { result.push(p + '-') } result.push(last) return result.filter(p => p.length > 0) } static splitWord(word) { if (typeof (language) == 'undefined') { if (word.indexOf('-') > -1) { return word.split('-') } return [word] } let parts = language.hyphenate(word) let result = [] for (let part of parts) { for (let splitted of this.splitPart(part)) { result.push(splitted) } } return result } static abbreviateLine(label, style, width) { const pixiStyle = new PIXI.TextStyle(style) let metrics = PIXI.TextMetrics.measureText(label, pixiStyle) while(metrics.width > width && label.length > 3) { label = label.slice(0, label.length-1) metrics = PIXI.TextMetrics.measureText(label, pixiStyle) } label = label.slice(0, label.length-1) return label + '…' } static splitLine(line, pixiStyle, width, space, minus) { let x = 0 let result = '' let words = line.split(' ') for (let word of words) { let wordMetrics = PIXI.TextMetrics.measureText(word, pixiStyle) if (x + wordMetrics.width >= width) { let parts = this.splitWord(word) let newWord = '' if (parts.length == 1) { newWord += '\n' + word + ' ' x = wordMetrics.width + space.width } else { let first = true let lastPart = '' for (let part of parts) { let partMetrics = PIXI.TextMetrics.measureText(part, pixiStyle) if (x + partMetrics.width + space.width > width) { newWord += ((first || lastPart.endsWith('-')) ? '\n' : '-\n') + part x = partMetrics.width } else { newWord += part x += partMetrics.width } lastPart = part first = false } x += space.width } result += newWord + ' ' } else { result += word + ' ' x += wordMetrics.width + space.width } } return result } /** * Main method and entry point for text hyphenation * * @static * @param {*} text * @param {*} style * @param {*} width * @returns * @memberof Hypenate */ static splitLines(text, style, width) { const pixiStyle = new PIXI.TextStyle(style) const lines = text.split('\n') const space = PIXI.TextMetrics.measureText(' ', pixiStyle) const minus = PIXI.TextMetrics.measureText('-', pixiStyle) let result = [] for (let line of lines) { result.push(this.splitLine(line, pixiStyle, width, space, minus)) } return result.join('\n') } } class TextLabel extends PIXI.Text { /** *Creates an instance of TextLabel. * @param {string} text - The string that you would like the text to display * @param {object PIXI.TextStyle optional} style - The style parameters * @param {canvas} * @memberof TextLabel */ constructor(text, style=null, canvas=null, { minZoom = 0.1, maxZoom = 10} = {}) { super(text, style, canvas ) this.normFontSize = this.style.fontSize this.minZoom = minZoom this.maxZoom = maxZoom } zoom(factor) { let oldValue = parseFloat(this.style.fontSize) / this.normFontSize let value = oldValue * factor this.setZoom(value) } setZoom(value) { let oldValue = parseFloat(this.style.fontSize) / this.normFontSize if (value > this.maxZoom) { value = this.maxZoom } if (value < this.minZoom) { value = this.minZoom } if (value != oldValue) { this.style.fontSize = Math.max(value * this.normFontSize, 1) } } setZoomAndScale(scale) { this.scale.set(1 / scale) this.setZoom(scale) } } /** * A specialization of the PIXI.Graphics class that allows to * resuse and place labels across different layout variants * * @export * @class LabeledGraphics * @extends {PIXI.Graphics} */ export class LabeledGraphics extends PIXI.Graphics { /** * Creates an instance of LabeledGraphics and defines a local label cache. * * @memberof LabeledGraphics */ constructor() { super() this.labels = new Map() } _createText(label, fontInfo) { return new TextLabel(label, fontInfo) } /** * Main additional method. Ensures that a text object is created that is cached * under the given key. * * @param {*} key - The cache key * @param {*} label - The label to show * @param {*} [attrs={}] - Defines attributes of the text object. * align: 'right', 'left', or 'center' * justify: 'top', 'bottom', or 'center' * maxLines: {integer} truncates the text and adds ellipsis * maxHeight: {number} truncates text that needs more space and adds ellipsis * maxWidth: {number} word wraps text using hyphenation if possible * @param {*} [fontInfo=FontInfo.normal] - Defines PIXI.TextStyle attributes * @returns {PIXI.Text} instance * @memberof LabeledGraphics */ ensureLabel(key, label, attrs = {}, fontInfo = FontInfo.normal) { if (attrs.maxWidth && attrs.maxLines == 1) { label = Hypenate.abbreviateLine(label, fontInfo, attrs.maxWidth) } else { if (attrs.maxWidth) { label = Hypenate.splitLines(label, fontInfo, attrs.maxWidth) } if (attrs.maxLines) { label = this.truncateLabel(label, fontInfo, attrs.maxLines) } if (attrs.maxHeight) { let styleInfo = new PIXI.TextStyle(fontInfo) let metrics = PIXI.TextMetrics.measureText(label, styleInfo) let maxLines = Math.max(attrs.maxHeight / metrics.lineHeight, 1) label = this.truncateLabel(label, fontInfo, maxLines) } } if (!this.labels.has(key)) { let text = this._createText(label, fontInfo) this.labels.set(key, text) this.addChild(text) } let text = this.labels.get(key) for (let k in attrs) { text[k] = attrs[k] } if (label != text.text) text.text = label // We do not follow the flexbox jargon and use align for x and justify for y axis // This deviation is needed to ensure backward compatability switch (attrs.justify || null) { case 'top': text.anchor.y = 0 break case 'bottom': text.anchor.x = 1 break default: text.anchor.y = 0.5 break } switch (attrs.align) { case 'right': text.anchor.x = 1 break case 'center': text.anchor.x = 0.5 break default: text.anchor.x = 0 break } text.visible = true return text } /** * Private method that truncates the text and adds an ellipsis if there are more lines * than wanted * * @param {*} text * @param {*} style * @param {*} [maxLines=Infinity] * @returns * @memberof LabeledGraphics */ truncateLabel(text, style, maxLines = Infinity) { if (maxLines === Infinity) { return text } const { wordWrapWidth } = style const pixiStyle = new PIXI.TextStyle(style) const { lines } = PIXI.TextMetrics.measureText(text, pixiStyle) let newText = text if (lines.length > maxLines) { const truncatedLines = lines.slice(0, maxLines) const lastLine = truncatedLines[truncatedLines.length - 1] const words = lastLine.split(' ') const wordMetrics = PIXI.TextMetrics.measureText(`\u00A0\n...\n${words.join('\n')}`, pixiStyle) const [spaceLength, dotsLength, ...wordLengths] = wordMetrics.lineWidths const { text: newLastLine } = wordLengths.reduce((data, wordLength, i) => { if (data.length + wordLength + spaceLength >= wordWrapWidth) { return { ...data, length: wordWrapWidth } } return { text: `${data.text}${i > 0 ? ' ' : ''}${words[i]}`, length: data.length + wordLength + spaceLength, }; }, { text: '', length: dotsLength }) truncatedLines[truncatedLines.length - 1] = `${newLastLine}...` newText = truncatedLines.join('\n') } return newText } /** * Returns the label for the given key. * * @param {*} key * @returns * @memberof LabeledGraphics */ getLabel(key) { return this.labels.get(key) } /** * Hides the label with the given key. * @param {*} key * @memberof LabeledGraphics */ hideLabel(key) { let label = this.labels.get(key) if (label) { label.visible = false } } /** * Removes the label with the given key. * @param {*} key * @memberof LabeledGraphics */ removeLabel(key) { let label = this.labels.get(key) this.labels.delete(key) label.destroy() } /** * Ensures that labels are hidden on clear. * * @memberof LabeledGraphics */ clear() { super.clear() for (let key of this.labels.keys()) { this.hideLabel(key) } } /** * Logs debugging infos * * @memberof LabeledGraphics */ debugInfos() { console.log({ size: this.labels.size, labels: this.labels }) } } const labelCache = new Map() function getTexture(label, fontInfo = FontInfo.normal) { let key = label + fontInfo.fontFamily + fontInfo.fontSize if (labelCache.has(key)) { return labelCache.get(key) } let expandedFont = Object.assign({}, fontInfo) expandedFont.fontSize *= window.devicePixelRatio let text = new PIXI.Text(label, expandedFont) text.updateText() labelCache.set(key, text.texture) return text.texture } class SpriteLabel extends PIXI.Sprite { constructor(label, fontInfo) { let texture = getTexture(label, fontInfo) super(texture) this.label = label this.fontInfo = fontInfo this.scale.set(0.8 / window.devicePixelRatio) } set text(label) { this.label = label this.texture = getTexture(label, this.fontInfo) } get text() { return this.label } } export class BitmapLabeledGraphics extends LabeledGraphics { _createText(label, fontInfo) { let texture = getTexture(label, fontInfo) return new SpriteLabel(texture) } }