s2maps-gpu
Version:
S2 Maps GPU - An open source, high-performance, and GPU-accelerated map engine for rendering large-scale, interactive maps.
357 lines (356 loc) • 13.8 kB
JavaScript
import { getPathPos } from '../util/index.js';
export const QUAD_SIZE_TEXT = 12;
export const QUAD_SIZE_PATH = 20;
export const NULL_GLYPH = {
code: '0',
texX: 0,
texY: 0,
texW: 0,
texH: 0,
xOffset: 0,
yOffset: 0,
width: 0,
height: 0,
advanceWidth: 0,
};
/**
* This step exclusively creates quad data, E.G. How to draw each glyph on the screen,
* given the anchor point as a basis for drawing. This step is seperate to preprocessing
* as we are avoiding doing too much work prior to potentially filtering the object (rtree).
* NOTE: EVERY GLYPH is currently "normalized", with a 0->1 scale so it can later be
* multiplied by "size"
* NOTE: Just put the glyph offsets + word-wrap-y offset provided at first,
* add in the excess anchor offset AFTER we know the bbox size
* TODO: https://blog.mapbox.com/beautifying-map-labels-with-better-line-breaking-2a6ce3ed432
* @param feature - input glyph point feature
* @param glyphSource - glyph source to pull glyph metadata from
* @param tileSize - tile size/extent
*/
export function buildGlyphPointQuads(feature, glyphSource, tileSize) {
const { max } = Math;
const { s, t, size, offset, padding, family, anchor, fieldCodes, wordWrap, align, kerning, lineHeight, type, quads, } = feature;
const [offsetX, offsetY] = offset;
const [paddingX, paddingY] = padding;
// setup variable
const rows = []; // a row: [glyph count, rowMaxWidth, rowMaxHeight]
let rowCount = 0;
let rowWidth = 0;
let rowHeight = 0;
let cursorX = 0;
// run through string using the glyphSet as a guide to build the quads
for (const unicode of fieldCodes) {
// word-wrap if line break or length exceeds max allowed.
if (type === 'text' && // is text
(unicode === '10' ||
unicode === '13' ||
(unicode === '32' && wordWrap !== 0 && cursorX >= wordWrap))) {
cursorX = 0;
const heightAdjust = rowCount > 0 ? rowHeight + lineHeight : 0;
updateGlyphPos(quads, 0, heightAdjust); // we move all previous content up a row
rows.push([rowCount, rowWidth, heightAdjust]);
rowCount = 0;
rowWidth = 0;
rowHeight = 0;
continue;
}
// grab the unicode information
const unicodeData = getGlyph(glyphSource, family, unicode);
if (unicodeData === undefined)
continue;
const { texX, texY, texW, texH, xOffset, yOffset, width, height, advanceWidth } = unicodeData;
// prep x-y positions
const xPos = cursorX + xOffset;
const yPos = yOffset;
if (texW > 0 && texH > 0) {
// store quad
quads.push(
// NOTE: offsetX and offsetY are the pixel based offset
// while xPos and yPos are the 0->1 glyph ratio placement
// position data
s, t, offsetX, offsetY, xPos, yPos, width, height,
// texture data
texX, texY, texW, texH);
// update number of glyphs and draw box height
rowCount++;
rowHeight = max(rowHeight, height);
}
// always update rowWidth by advanceWidth
rowWidth = max(rowWidth, xPos + width, xPos + advanceWidth);
// advance cursor position
cursorX += advanceWidth + kerning;
}
// store the last row
rows.push([rowCount, rowWidth, rowCount > 0 ? rowHeight + lineHeight : 0]);
// grab max width from said
const maxWidth = rows.reduce((acc, curr) => max(acc, curr[1]), 0);
const maxHeight = rows.reduce((acc, curr) => acc + curr[2], 0);
// adjust text based upon center-align or right-align
alignText(align, quads, rows, maxWidth);
// now adjust all glyphs and max-values by the anchor and alignment
const [anchorOffsetX, anchorOffsetY] = anchorOffset(anchor, maxWidth, maxHeight);
updateGlyphPos(quads, anchorOffsetX, anchorOffsetY);
// set minX, maxX, minY, maxY in the feature
feature.minX = s * tileSize + offsetX + anchorOffsetX * size - paddingX;
feature.minY = t * tileSize + offsetY + anchorOffsetY * size - paddingY;
feature.maxX = feature.minX + maxWidth * size + paddingX * 2;
feature.maxY = feature.minY + maxHeight * size + paddingY * 2;
// store the filter
feature.filter = [
s,
t,
anchorOffsetX,
anchorOffsetY,
offsetX,
offsetY,
paddingX,
paddingY,
maxWidth,
maxHeight,
];
}
/**
* Build glyph path quads
* IDEATION: https://blog.mapbox.com/map-label-placement-in-mapbox-gl-c6f843a7caaa
* @param feature - glyph lines feature
* @param glyphSource - glyph source to pull glyph metadata from
* @param tileSize - tile size/extent
*/
export function buildGlyphPathQuads(feature, glyphSource, tileSize) {
const { max } = Math;
// NOTE: missing "size" and "padding"
const { size, offset, family, anchor, pathData, align, kerning } = feature;
let { fieldCodes } = feature;
const [offsetX, offsetY] = offset;
const padding = max(...feature.padding);
const { point: { x: s, y: t }, pathLeft, pathRight, } = pathData;
// first replace all newlines with spaces
fieldCodes = fieldCodes.map((unicode) => {
if (unicode === '10' || unicode === '13')
return '32';
else
return unicode;
});
// setup variable
let maxWidth = 0;
let maxHeight = 0;
let cursorX = 0;
// run through string using the glyphSet as a guide to build the quads
const quads = [];
for (const unicode of fieldCodes) {
// grab the unicode information
const unicodeData = getGlyph(glyphSource, family, unicode);
if (unicodeData === undefined)
continue;
const { texX, texY, texW, texH, xOffset, yOffset, width, height, advanceWidth } = unicodeData;
maxHeight = max(maxHeight, height);
// prep x-y positions & distance
const xPos = cursorX + xOffset + width / 2;
const yPos = yOffset + height / 2;
// skip no texture data like spaces
if (texW > 0 && texH > 0) {
// NOTE: texX, texY, texW, texH, offsetX, and offsetY are the pixel based values
// while xPos, yPos, width, and height are the 0->1 ratio placement to user defined size
// st is 0->1 ratio relative to tile size
quads.push(
// position data
s, t, offsetX, offsetY, xPos, yPos, width, height,
// texture data
texX, texY, texW, texH,
// tmp path data
0, 0, 0, 0, 0, 0, 0, 0);
}
maxWidth = max(maxWidth, xPos + width, xPos + advanceWidth);
// advance cursor position
cursorX += advanceWidth + kerning;
}
// adjust text based upon center-align or right-align
alignText(align, quads, [[0, maxWidth, 0]], maxWidth, 'path');
// now adjust all glyphs and max-values by the anchor and alignment
const [anchorOffsetX, anchorOffsetY] = anchorOffsetPath(anchor, maxWidth, maxHeight);
updateGlyphPos(quads, anchorOffsetX, anchorOffsetY, 'path');
// set the correct path data relative to whether the glyph is traveling
// left or right from the anchor point
updatePathData(quads, pathLeft, pathRight);
// add nodes to the feature for each quad
buildFeatureNodes(feature, quads, pathLeft, pathRight, size, padding, tileSize);
// last step is to build the filter container paths. For each quad just
// copy paste a slice from st to end of path, add padding, and store to filters
storePathFeatureFilters(feature, quads, padding);
// store quads in feature
feature.quads.push(...quads);
}
/**
* Get a glyph's metadata from the glyph source
* @param glyphSource - glyph source
* @param family - glyph family
* @param code - glyph code
* @returns the glyph metadata
*/
function getGlyph(glyphSource, family, code) {
for (const familyName of family) {
const glyphFamily = glyphSource.get(familyName);
if (glyphFamily === undefined)
continue;
const glyph = glyphFamily.glyphCache.get(code);
if (glyph !== undefined)
return glyph;
}
return NULL_GLYPH;
}
/**
* Update the position of the glyphs
* @param quads - glyph quads
* @param offsetX - x offset
* @param offsetY - y offset
* @param glyphType - glyph type
*/
function updateGlyphPos(quads, offsetX, offsetY, glyphType = 'text') {
const quadSize = glyphType === 'text' ? QUAD_SIZE_TEXT : QUAD_SIZE_PATH;
for (let i = 0, ql = quads.length; i < ql; i += quadSize) {
quads[i + 4] += offsetX;
quads[i + 5] += offsetY;
}
}
/**
* Set an user defined anchor offset. boxes start at the bottom left as UV [0,0] to [1, 1]
* @param anchor - anchor type
* @param width - full width of all glyphs in the line or paragrpah
* @param height - full height of all glyphs in the line or paragrpah
* @returns the anchor offset
*/
function anchorOffset(anchor, width, height) {
if (anchor === 'center')
return [-width / 2, -height / 2];
else if (anchor === 'top')
return [-width / 2, -height];
else if (anchor === 'top-right')
return [-width, -height];
else if (anchor === 'right')
return [-width, -height / 2];
else if (anchor === 'bottom-right')
return [-width, 0];
else if (anchor === 'bottom')
return [-width / 2, 0];
else if (anchor === 'bottom-left')
return [0, 0];
else if (anchor === 'left')
return [0, -height / 2];
else if (anchor === 'top-left')
return [0, -height];
else
return [-width / 2, -height / 2]; // default to center
}
/**
* Set a user defined anchor offset for path glyphs
* the path drawing takes [-0.5, -0.5] to [0.5, 0.5] quads
* @param anchor - anchor type
* @param width - full width of all glyphs in the line or paragrpah
* @param height - full height of all glyphs in the line or paragrpah
* @returns the anchor offset
*/
function anchorOffsetPath(anchor, width, height) {
if (anchor === 'center')
return [-width / 2, 0];
else if (anchor === 'top')
return [-width / 2, -height / 2];
else if (anchor === 'top-right')
return [-width, -height / 2];
else if (anchor === 'right')
return [-width, 0];
else if (anchor === 'bottom-right')
return [-width, height / 2];
else if (anchor === 'bottom')
return [-width / 2, height / 2];
else if (anchor === 'bottom-left')
return [0, height / 2];
else if (anchor === 'left')
return [0, 0];
else if (anchor === 'top-left')
return [0, -height / 2];
else
return [-width / 2, 0]; // default to center
}
/**
* Align text given a user defined alignment
* @param align - alignment
* @param quads - glyph quads
* @param rows - rows and their size,width,& height
* @param maxWidth - max width found from all rows
* @param glyphType - glyph type (text or path)
*/
function alignText(align, quads, rows, maxWidth, glyphType = 'text') {
const quadSize = glyphType === 'text' ? QUAD_SIZE_TEXT : QUAD_SIZE_PATH;
if (align !== 'center' && align !== 'right')
return;
const alignFunc = align === 'center'
? (mW, rW) => (mW - rW) / 2 // center align
: (mW, rW) => mW - rW; // right align
let currPos = 0;
let idx = 0;
// iterate rows, grab their count and width, adjust as necessary
for (const [rowCount, rowWidth] of rows) {
// if row is same size as the width of the display box, move on
if (rowWidth === maxWidth) {
currPos += rowCount;
continue;
}
// find the alignment based upon the rows width and the total draw box width
const adjust = alignFunc(maxWidth, rowWidth);
// iterate the rows, adding the x-y adjustment as appropriate
for (let i = 0; i < rowCount; i++) {
idx = currPos * quadSize;
quads[idx + 4] += adjust;
currPos++;
}
}
}
/**
* Update path data's x and y
* @param quads - glyph quads
* @param pathLeft - left path
* @param pathRight - right path
*/
function updatePathData(quads, pathLeft, pathRight) {
for (let i = 0, ql = quads.length; i < ql; i += QUAD_SIZE_PATH) {
const path = quads[i + 4] >= 0 ? pathRight : pathLeft;
for (let j = 0; j < 4; j++) {
quads[i + 12 + j * 2] = path[j].x;
quads[i + 13 + j * 2] = path[j].y;
}
}
}
/**
* Build feature nodes
* @param feature - glyph feature
* @param quads - glyph quads
* @param pathLeft - left path
* @param pathRight - right path
* @param size - glyph size
* @param padding - glyph padding
* @param tileSize - tile size
*/
function buildFeatureNodes(feature, quads, pathLeft, pathRight, size, padding, tileSize) {
for (let i = 0, ql = quads.length; i < ql; i += QUAD_SIZE_PATH) {
const quadPos = quads.slice(i, i + 6);
const { x, y } = getPathPos(quadPos, pathLeft, pathRight, tileSize, size);
feature.nodes.push({ x, y, r: size / 2 + padding });
}
}
/**
* Store path feature's filters for the GPU to filter overalapping glyphs
* @param feature - glyph feature
* @param quads - glyph quads
* @param padding - glyph padding
*/
function storePathFeatureFilters(feature, quads, padding) {
for (let i = 0, ql = quads.length; i < ql; i += QUAD_SIZE_PATH) {
feature.filters.push([
// st, offsetXY, xy
...quads.slice(i, i + 6),
// paths
...quads.slice(i + 12, i + 20),
// padding
padding,
]);
}
}