399 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			399 lines
		
	
	
		
			12 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
 | 
						|
     * @memberof Hypenate
 | 
						|
     * @returns {string}
 | 
						|
     */
 | 
						|
    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} [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 {string}
 | 
						|
     * @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 {Object}
 | 
						|
     * @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)
 | 
						|
    }
 | 
						|
}
 |