Source: packages/text-bitmap/src/BitmapFont.js

packages/text-bitmap/src/BitmapFont.js

import { getResolutionOfUrl } from '@pixi/utils';
import { Rectangle } from '@pixi/math';
import { Texture, BaseTexture } from '@pixi/core';
import { TextStyle, TextMetrics } from '@pixi/text';
import { autoDetectFormat } from './formats';
import { BitmapFontData } from './BitmapFontData';
import { resolveCharacters, drawGlyph } from './utils';
/**
 * BitmapFont represents a typeface available for use with the BitmapText class. Use the `install`
 * method for adding a font to be used.
 *
 * @class
 * @memberof PIXI
 */
export class BitmapFont {
    /**
     * @param {PIXI.BitmapFontData} data
     * @param {PIXI.Texture[]|Object.<string, PIXI.Texture>} textures
     */
    constructor(data, textures) {
        const [info] = data.info;
        const [common] = data.common;
        const [page] = data.page;
        const res = getResolutionOfUrl(page.file);
        const pageTextures = {};
        /**
         * The name of the font face.
         *
         * @member {string}
         * @readonly
         */
        this.font = info.face;
        /**
         * The size of the font face in pixels.
         *
         * @member {number}
         * @readonly
         */
        this.size = info.size;
        /**
         * The line-height of the font face in pixels.
         *
         * @member {number}
         * @readonly
         */
        this.lineHeight = common.lineHeight / res;
        /**
         * The map of characters by character code.
         *
         * @member {object}
         * @readonly
         */
        this.chars = {};
        /**
         * The map of base page textures (i.e., sheets of glyphs).
         *
         * @member {object}
         * @readonly
         * @private
         */
        this.pageTextures = pageTextures;
        // Convert the input Texture, Textures or object
        // into a page Texture lookup by "id"
        for (let i = 0; i < data.page.length; i++) {
            const { id, file } = data.page[i];
            pageTextures[id] = textures instanceof Array
                ? textures[i] : textures[file];
        }
        // parse letters
        for (let i = 0; i < data.char.length; i++) {
            const { id, page } = data.char[i];
            let { x, y, width, height, xoffset, yoffset, xadvance } = data.char[i];
            x /= res;
            y /= res;
            width /= res;
            height /= res;
            xoffset /= res;
            yoffset /= res;
            xadvance /= res;
            const rect = new Rectangle(x + (pageTextures[page].frame.x / res), y + (pageTextures[page].frame.y / res), width, height);
            this.chars[id] = {
                xOffset: xoffset,
                yOffset: yoffset,
                xAdvance: xadvance,
                kerning: {},
                texture: new Texture(pageTextures[page].baseTexture, rect),
                page,
            };
        }
        // parse kernings
        for (let i = 0; i < data.kerning.length; i++) {
            let { first, second, amount } = data.kerning[i];
            first /= res;
            second /= res;
            amount /= res;
            if (this.chars[second]) {
                this.chars[second].kerning[first] = amount;
            }
        }
    }
    /**
     * Remove references to created glyph textures.
     */
    destroy() {
        for (const id in this.chars) {
            this.chars[id].texture.destroy();
            this.chars[id].texture = null;
        }
        for (const id in this.pageTextures) {
            this.pageTextures[id].destroy(true);
            this.pageTextures[id] = null;
        }
        // Set readonly null.
        this.chars = null;
        this.pageTextures = null;
    }
    /**
     * Register a new bitmap font.
     *
     * @static
     * @param {XMLDocument|string|PIXI.BitmapFontData} data - The
     *        characters map that could be provided as xml or raw string.
     * @param {Object.<string, PIXI.Texture>|PIXI.Texture|PIXI.Texture[]}
     *        textures - List of textures for each page.
     * @return {PIXI.BitmapFont} Result font object with font, size, lineHeight
     *         and char fields.
     */
    static install(data, textures) {
        let fontData;
        if (data instanceof BitmapFontData) {
            fontData = data;
        }
        else {
            const format = autoDetectFormat(data);
            if (!format) {
                throw new Error('Unrecognized data format for font.');
            }
            fontData = format.parse(data);
        }
        // Single texture, convert to list
        if (textures instanceof Texture) {
            textures = [textures];
        }
        const font = new BitmapFont(fontData, textures);
        BitmapFont.available[font.font] = font;
        return font;
    }
    /**
     * Remove bitmap font by name.
     *
     * @static
     * @param {string} name
     */
    static uninstall(name) {
        const font = BitmapFont.available[name];
        if (!font) {
            throw new Error(`No font found named '${name}'`);
        }
        font.destroy();
        delete BitmapFont.available[name];
    }
    /**
     * Generates a bitmap-font for the given style and character set. This does not support
     * kernings yet. With `style` properties, only the following non-layout properties are used:
     *
     * - {@link PIXI.TextStyle#dropShadow|dropShadow}
     * - {@link PIXI.TextStyle#dropShadowDistance|dropShadowDistance}
     * - {@link PIXI.TextStyle#dropShadowColor|dropShadowColor}
     * - {@link PIXI.TextStyle#dropShadowBlur|dropShadowBlur}
     * - {@link PIXI.TextStyle#dropShadowAngle|dropShadowAngle}
     * - {@link PIXI.TextStyle#fill|fill}
     * - {@link PIXI.TextStyle#fillGradientStops|fillGradientStops}
     * - {@link PIXI.TextStyle#fillGradientType|fillGradientType}
     * - {@link PIXI.TextStyle#fontFamily|fontFamily}
     * - {@link PIXI.TextStyle#fontSize|fontSize}
     * - {@link PIXI.TextStyle#fontVariant|fontVariant}
     * - {@link PIXI.TextStyle#fontWeight|fontWeight}
     * - {@link PIXI.TextStyle#lineJoin|lineJoin}
     * - {@link PIXI.TextStyle#miterLimit|miterLimit}
     * - {@link PIXI.TextStyle#stroke|stroke}
     * - {@link PIXI.TextStyle#strokeThickness|strokeThickness}
     * - {@link PIXI.TextStyle#textBaseline|textBaseline}
     *
     * @param {string} name - The name of the custom font to use with BitmapText.
     * @param {object|PIXI.TextStyle} [style] - Style options to render with BitmapFont.
     * @param {PIXI.IBitmapFontOptions} [options] - Setup options for font or name of the font.
     * @param {string|string[]|string[][]} [options.chars=PIXI.BitmapFont.ALPHANUMERIC] - characters included
     *      in the font set. You can also use ranges. For example, `[['a', 'z'], ['A', 'Z'], "!@#$%^&*()~{}[] "]`.
     *      Don't forget to include spaces ' ' in your character set!
     * @param {number} [options.resolution=1] - Render resolution for glyphs.
     * @param {number} [options.textureWidth=512] - Optional width of atlas, smaller values to reduce memory.
     * @param {number} [options.textureHeight=512] - Optional height of atlas, smaller values to reduce memory.
     * @param {number} [options.padding=4] - Padding between glyphs on texture atlas.
     * @return {PIXI.BitmapFont} Font generated by style options.
     * @static
     * @example
     * PIXI.BitmapFont.from("TitleFont", {
     *     fontFamily: "Arial",
     *     fontSize: 12,
     *     strokeThickness: 2,
     *     fill: "purple"
     * });
     *
     * const title = new PIXI.BitmapText("This is the title", { fontName: "TitleFont" });
     */
    static from(name, textStyle, options) {
        if (!name) {
            throw new Error('[BitmapFont] Property `name` is required.');
        }
        const { chars, padding, resolution, textureWidth, textureHeight } = Object.assign({}, BitmapFont.defaultOptions, options);
        const charsList = resolveCharacters(chars);
        const style = textStyle instanceof TextStyle ? textStyle : new TextStyle(textStyle);
        const lineWidth = textureWidth;
        const fontData = new BitmapFontData();
        fontData.info[0] = {
            face: style.fontFamily,
            size: style.fontSize,
        };
        fontData.common[0] = {
            lineHeight: style.fontSize,
        };
        let positionX = 0;
        let positionY = 0;
        let canvas;
        let context;
        let baseTexture;
        let maxCharHeight = 0;
        const baseTextures = [];
        const textures = [];
        for (let i = 0; i < charsList.length; i++) {
            if (!canvas) {
                canvas = document.createElement('canvas');
                canvas.width = textureWidth;
                canvas.height = textureHeight;
                context = canvas.getContext('2d');
                baseTexture = new BaseTexture(canvas, { resolution });
                baseTextures.push(baseTexture);
                textures.push(new Texture(baseTexture));
                fontData.page.push({
                    id: textures.length - 1,
                    file: '',
                });
            }
            // Measure glyph dimensions
            const metrics = TextMetrics.measureText(charsList[i], style, false, canvas);
            const width = metrics.width;
            const height = Math.ceil(metrics.height);
            // This is ugly - but italics are given more space so they don't overlap
            const textureGlyphWidth = Math.ceil((style.fontStyle === 'italic' ? 2 : 1) * width);
            // Can't fit char anymore: next canvas please!
            if (positionY >= textureHeight - (height * resolution)) {
                if (positionY === 0) {
                    // We don't want user debugging an infinite loop (or do we? :)
                    throw new Error(`[BitmapFont] textureHeight ${textureHeight}px is `
                        + `too small for ${style.fontSize}px fonts`);
                }
                --i;
                // Create new atlas once current has filled up
                canvas = null;
                context = null;
                baseTexture = null;
                positionY = 0;
                positionX = 0;
                maxCharHeight = 0;
                continue;
            }
            maxCharHeight = Math.max(height + metrics.fontProperties.descent, maxCharHeight);
            // Wrap line once full row has been rendered
            if ((textureGlyphWidth * resolution) + positionX >= lineWidth) {
                --i;
                positionY += maxCharHeight * resolution;
                positionY = Math.ceil(positionY);
                positionX = 0;
                maxCharHeight = 0;
                continue;
            }
            drawGlyph(canvas, context, metrics, positionX, positionY, resolution, style);
            // Unique (numeric) ID mapping to this glyph
            const id = metrics.text.charCodeAt(0);
            // Create a texture holding just the glyph
            fontData.char.push({
                id,
                page: textures.length - 1,
                x: positionX / resolution,
                y: positionY / resolution,
                width: textureGlyphWidth,
                height,
                xoffset: 0,
                yoffset: 0,
                xadvance: Math.ceil(width
                    - (style.dropShadow ? style.dropShadowDistance : 0)
                    - (style.stroke ? style.strokeThickness : 0)),
            });
            positionX += (textureGlyphWidth + (2 * padding)) * resolution;
            positionX = Math.ceil(positionX);
        }
        const font = new BitmapFont(fontData, textures);
        // Make it easier to replace a font
        if (BitmapFont.available[name] !== undefined) {
            BitmapFont.uninstall(name);
        }
        BitmapFont.available[name] = font;
        return font;
    }
}
/**
 * This character set includes all the letters in the alphabet (both lower- and upper- case).
 * @readonly
 * @static
 * @member {string[][]}
 * @example
 * BitmapFont.from("ExampleFont", style, { chars: BitmapFont.ALPHA })
 */
BitmapFont.ALPHA = [['a', 'z'], ['A', 'Z'], ' '];
/**
 * This character set includes all decimal digits (from 0 to 9).
 * @readonly
 * @static
 * @member {string[][]}
 * @example
 * BitmapFont.from("ExampleFont", style, { chars: BitmapFont.NUMERIC })
 */
BitmapFont.NUMERIC = [['0', '9']];
/**
 * This character set is the union of `BitmapFont.ALPHA` and `BitmapFont.NUMERIC`.
 * @readonly
 * @static
 * @member {string[][]}
 */
BitmapFont.ALPHANUMERIC = [['a', 'z'], ['A', 'Z'], ['0', '9'], ' '];
/**
 * This character set consists of all the ASCII table.
 * @readonly
 * @static
 * @member {string[][]}
 * @see http://www.asciitable.com/
 */
BitmapFont.ASCII = [[' ', '~']];
/**
 * Collection of default options when using `BitmapFont.from`.
 *
 * @readonly
 * @static
 * @member {PIXI.IBitmapFontOptions}
 * @property {number} resolution=1
 * @property {number} textureWidth=512
 * @property {number} textureHeight=512
 * @property {number} padding=4
 * @property {string|string[]|string[][]} chars=PIXI.BitmapFont.ALPHANUMERIC
 */
BitmapFont.defaultOptions = {
    resolution: 1,
    textureWidth: 512,
    textureHeight: 512,
    padding: 4,
    chars: BitmapFont.ALPHANUMERIC,
};
/**
 * Collection of available/installed fonts.
 *
 * @readonly
 * @static
 * @member {Object.<string, PIXI.BitmapFont>}
 */
BitmapFont.available = {};
/**
 * @memberof PIXI
 * @interface IBitmapFontOptions
 * @property {string | string[] | string[][]} [chars=PIXI.BitmapFont.ALPHANUMERIC] - the character set to generate
 * @property {number} [resolution=1] - the resolution for rendering
 * @property {number} [padding=4] - the padding between glyphs in the atlas
 * @property {number} [textureWidth=512] - the width of the texture atlas
 * @property {number} [textureHeight=512] - the height of the texture atlas
 */