layout-bmfont-text
Version:
word-wraps and lays out text glyphs
299 lines (253 loc) • 7.11 kB
JavaScript
var wordWrap = require('word-wrapper')
var xtend = require('xtend')
var number = require('as-number')
var X_HEIGHTS = ['x', 'e', 'a', 'o', 'n', 's', 'r', 'c', 'u', 'm', 'v', 'w', 'z']
var M_WIDTHS = ['m', 'w']
var CAP_HEIGHTS = ['H', 'I', 'N', 'E', 'F', 'K', 'L', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']
var TAB_ID = '\t'.charCodeAt(0)
var SPACE_ID = ' '.charCodeAt(0)
var ALIGN_LEFT = 0,
ALIGN_CENTER = 1,
ALIGN_RIGHT = 2
module.exports = function createLayout(opt) {
return new TextLayout(opt)
}
function TextLayout(opt) {
this.glyphs = []
this._measure = this.computeMetrics.bind(this)
this.update(opt)
}
TextLayout.prototype.update = function(opt) {
opt = xtend({
measure: this._measure
}, opt)
this._opt = opt
this._opt.tabSize = number(this._opt.tabSize, 4)
if (!opt.font)
throw new Error('must provide a valid bitmap font')
var glyphs = this.glyphs
var text = opt.text||''
var font = opt.font
this._setupSpaceGlyphs(font)
var lines = wordWrap.lines(text, opt)
var minWidth = opt.width || 0
//clear glyphs
glyphs.length = 0
//get max line width
var maxLineWidth = lines.reduce(function(prev, line) {
return Math.max(prev, line.width, minWidth)
}, 0)
//the pen position
var x = 0
var y = 0
var lineHeight = number(opt.lineHeight, font.common.lineHeight)
var baseline = font.common.base
var descender = lineHeight-baseline
var letterSpacing = opt.letterSpacing || 0
var height = lineHeight * lines.length - descender
var align = getAlignType(this._opt.align)
//draw text along baseline
y -= height
//the metrics for this text layout
this._width = maxLineWidth
this._height = height
this._descender = lineHeight - baseline
this._baseline = baseline
this._xHeight = getXHeight(font)
this._capHeight = getCapHeight(font)
this._lineHeight = lineHeight
this._ascender = lineHeight - descender - this._xHeight
//layout each glyph
var self = this
lines.forEach(function(line, lineIndex) {
var start = line.start
var end = line.end
var lineWidth = line.width
var lastGlyph
//for each glyph in that line...
for (var i=start; i<end; i++) {
var id = text.charCodeAt(i)
var glyph = self.getGlyph(font, id)
if (glyph) {
if (lastGlyph)
x += getKerning(font, lastGlyph.id, glyph.id)
var tx = x
if (align === ALIGN_CENTER)
tx += (maxLineWidth-lineWidth)/2
else if (align === ALIGN_RIGHT)
tx += (maxLineWidth-lineWidth)
glyphs.push({
position: [tx, y],
data: glyph,
index: i,
line: lineIndex
})
//move pen forward
x += glyph.xadvance + letterSpacing
lastGlyph = glyph
}
}
//next line down
y += lineHeight
x = 0
})
this._linesTotal = lines.length;
}
TextLayout.prototype._setupSpaceGlyphs = function(font) {
//These are fallbacks, when the font doesn't include
//' ' or '\t' glyphs
this._fallbackSpaceGlyph = null
this._fallbackTabGlyph = null
if (!font.chars || font.chars.length === 0)
return
//try to get space glyph
//then fall back to the 'm' or 'w' glyphs
//then fall back to the first glyph available
var space = getGlyphById(font, SPACE_ID)
|| getMGlyph(font)
|| font.chars[0]
//and create a fallback for tab
var tabWidth = this._opt.tabSize * space.xadvance
this._fallbackSpaceGlyph = space
this._fallbackTabGlyph = xtend(space, {
x: 0, y: 0, xadvance: tabWidth, id: TAB_ID,
xoffset: 0, yoffset: 0, width: 0, height: 0
})
}
TextLayout.prototype.getGlyph = function(font, id) {
var glyph = getGlyphById(font, id)
if (glyph)
return glyph
else if (id === TAB_ID)
return this._fallbackTabGlyph
else if (id === SPACE_ID)
return this._fallbackSpaceGlyph
return null
}
TextLayout.prototype.computeMetrics = function(text, start, end, width) {
var letterSpacing = this._opt.letterSpacing || 0
var font = this._opt.font
var curPen = 0
var curWidth = 0
var count = 0
var glyph
var lastGlyph
if (!font.chars || font.chars.length === 0) {
return {
start: start,
end: start,
width: 0
}
}
end = Math.min(text.length, end)
for (var i=start; i < end; i++) {
var id = text.charCodeAt(i)
var glyph = this.getGlyph(font, id)
if (glyph) {
//move pen forward
var xoff = glyph.xoffset
var kern = lastGlyph ? getKerning(font, lastGlyph.id, glyph.id) : 0
curPen += kern
var nextPen = curPen + glyph.xadvance + letterSpacing
var nextWidth = curPen + glyph.width
//we've hit our limit; we can't move onto the next glyph
if (nextWidth >= width || nextPen >= width)
break
//otherwise continue along our line
curPen = nextPen
curWidth = nextWidth
lastGlyph = glyph
}
count++
}
//make sure rightmost edge lines up with rendered glyphs
if (lastGlyph)
curWidth += lastGlyph.xoffset
return {
start: start,
end: start + count,
width: curWidth
}
}
//getters for the private vars
;['width', 'height',
'descender', 'ascender',
'xHeight', 'baseline',
'capHeight',
'lineHeight' ].forEach(addGetter)
function addGetter(name) {
Object.defineProperty(TextLayout.prototype, name, {
get: wrapper(name),
configurable: true
})
}
//create lookups for private vars
function wrapper(name) {
return (new Function([
'return function '+name+'() {',
' return this._'+name,
'}'
].join('\n')))()
}
function getGlyphById(font, id) {
if (!font.chars || font.chars.length === 0)
return null
var glyphIdx = findChar(font.chars, id)
if (glyphIdx >= 0)
return font.chars[glyphIdx]
return null
}
function getXHeight(font) {
for (var i=0; i<X_HEIGHTS.length; i++) {
var id = X_HEIGHTS[i].charCodeAt(0)
var idx = findChar(font.chars, id)
if (idx >= 0)
return font.chars[idx].height
}
return 0
}
function getMGlyph(font) {
for (var i=0; i<M_WIDTHS.length; i++) {
var id = M_WIDTHS[i].charCodeAt(0)
var idx = findChar(font.chars, id)
if (idx >= 0)
return font.chars[idx]
}
return 0
}
function getCapHeight(font) {
for (var i=0; i<CAP_HEIGHTS.length; i++) {
var id = CAP_HEIGHTS[i].charCodeAt(0)
var idx = findChar(font.chars, id)
if (idx >= 0)
return font.chars[idx].height
}
return 0
}
function getKerning(font, left, right) {
if (!font.kernings || font.kernings.length === 0)
return 0
var table = font.kernings
for (var i=0; i<table.length; i++) {
var kern = table[i]
if (kern.first === left && kern.second === right)
return kern.amount
}
return 0
}
function getAlignType(align) {
if (align === 'center')
return ALIGN_CENTER
else if (align === 'right')
return ALIGN_RIGHT
return ALIGN_LEFT
}
function findChar (array, value, start) {
start = start || 0
for (var i = start; i < array.length; i++) {
if (array[i].id === value) {
return i
}
}
return -1
}