373 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			373 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 
 | |
| 
 | |
| /**
 | |
|  * 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')
 | |
|     }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * 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 PIXI.Text(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)
 | |
|     }
 | |
| 
 | |
| } |