mapbox-gl
Version:
A WebGL interactive maps library
180 lines (136 loc) • 5.49 kB
JavaScript
'use strict';
module.exports = {
shapeText: shapeText,
shapeIcon: shapeIcon
};
// The position of a glyph relative to the text's anchor point.
function PositionedGlyph(codePoint, x, y, glyph) {
this.codePoint = codePoint;
this.x = x;
this.y = y;
this.glyph = glyph;
}
// A collection of positioned glyphs and some metadata
function Shaping(positionedGlyphs, text, top, bottom, left, right) {
this.positionedGlyphs = positionedGlyphs;
this.text = text;
this.top = top;
this.bottom = bottom;
this.left = left;
this.right = right;
}
function shapeText(text, glyphs, maxWidth, lineHeight, horizontalAlign, verticalAlign, justify, spacing, translate) {
var positionedGlyphs = [];
var shaping = new Shaping(positionedGlyphs, text, translate[1], translate[1], translate[0], translate[0]);
// the y offset *should* be part of the font metadata
var yOffset = -17;
var x = 0;
var y = yOffset;
for (var i = 0; i < text.length; i++) {
var codePoint = text.charCodeAt(i);
var glyph = glyphs[codePoint];
if (!glyph) continue;
positionedGlyphs.push(new PositionedGlyph(codePoint, x, y, glyph));
x += glyph.advance + spacing;
}
if (!positionedGlyphs.length) return false;
linewrap(shaping, glyphs, lineHeight, maxWidth, horizontalAlign, verticalAlign, justify, translate);
return shaping;
}
var invisible = {
0x20: true, // space
0x200b: true // zero-width space
};
var breakable = {
0x20: true, // space
0x26: true, // ampersand
0x2b: true, // plus sign
0x2d: true, // hyphen-minus
0x2f: true, // solidus
0xad: true, // soft hyphen
0xb7: true, // middle dot
0x200b: true, // zero-width space
0x2010: true, // hyphen
0x2013: true // en dash
};
function linewrap(shaping, glyphs, lineHeight, maxWidth, horizontalAlign, verticalAlign, justify, translate) {
var lastSafeBreak = null;
var lengthBeforeCurrentLine = 0;
var lineStartIndex = 0;
var line = 0;
var maxLineLength = 0;
var positionedGlyphs = shaping.positionedGlyphs;
if (maxWidth) {
for (var i = 0; i < positionedGlyphs.length; i++) {
var positionedGlyph = positionedGlyphs[i];
positionedGlyph.x -= lengthBeforeCurrentLine;
positionedGlyph.y += lineHeight * line;
if (positionedGlyph.x > maxWidth && lastSafeBreak !== null) {
var lineLength = positionedGlyphs[lastSafeBreak + 1].x;
maxLineLength = Math.max(lineLength, maxLineLength);
for (var k = lastSafeBreak + 1; k <= i; k++) {
positionedGlyphs[k].y += lineHeight;
positionedGlyphs[k].x -= lineLength;
}
if (justify) {
// Collapse invisible characters.
var lineEnd = lastSafeBreak;
if (invisible[positionedGlyphs[lastSafeBreak].codePoint]) {
lineEnd--;
}
justifyLine(positionedGlyphs, glyphs, lineStartIndex, lineEnd, justify);
}
lineStartIndex = lastSafeBreak + 1;
lastSafeBreak = null;
lengthBeforeCurrentLine += lineLength;
line++;
}
if (breakable[positionedGlyph.codePoint]) {
lastSafeBreak = i;
}
}
}
var lastPositionedGlyph = positionedGlyphs[positionedGlyphs.length - 1];
var lastLineLength = lastPositionedGlyph.x + glyphs[lastPositionedGlyph.codePoint].advance;
maxLineLength = Math.max(maxLineLength, lastLineLength);
var height = (line + 1) * lineHeight;
justifyLine(positionedGlyphs, glyphs, lineStartIndex, positionedGlyphs.length - 1, justify);
align(positionedGlyphs, justify, horizontalAlign, verticalAlign, maxLineLength, lineHeight, line, translate);
// Calculate the bounding box
shaping.top += -verticalAlign * height;
shaping.bottom = shaping.top + height;
shaping.left += -horizontalAlign * maxLineLength;
shaping.right = shaping.left + maxLineLength;
}
function justifyLine(positionedGlyphs, glyphs, start, end, justify) {
var lastAdvance = glyphs[positionedGlyphs[end].codePoint].advance;
var lineIndent = (positionedGlyphs[end].x + lastAdvance) * justify;
for (var j = start; j <= end; j++) {
positionedGlyphs[j].x -= lineIndent;
}
}
function align(positionedGlyphs, justify, horizontalAlign, verticalAlign, maxLineLength, lineHeight, line, translate) {
var shiftX = (justify - horizontalAlign) * maxLineLength + translate[0];
var shiftY = (-verticalAlign * (line + 1) + 0.5) * lineHeight + translate[1];
for (var j = 0; j < positionedGlyphs.length; j++) {
positionedGlyphs[j].x += shiftX;
positionedGlyphs[j].y += shiftY;
}
}
function shapeIcon(image, layout) {
if (!image || !image.rect) return null;
var dx = layout['icon-offset'][0];
var dy = layout['icon-offset'][1];
var x1 = dx - image.width / 2;
var x2 = x1 + image.width;
var y1 = dy - image.height / 2;
var y2 = y1 + image.height;
return new PositionedIcon(image, y1, y2, x1, x2);
}
function PositionedIcon(image, top, bottom, left, right) {
this.image = image;
this.top = top;
this.bottom = bottom;
this.left = left;
this.right = right;
}