Skip to content

Commit

Permalink
Add support for rendering CJK in a vertical writing mode along line-p…
Browse files Browse the repository at this point in the history
…laced features (#3438)

* Add support for ideographic text breaking

* Sorted CJK punctuation by codepoint

* Mapped more punctuation to fullwidth equivalents

* Mapped middle dot to fullwidth
  • Loading branch information
lucaswoj authored Nov 10, 2016
1 parent 1926aa5 commit 810e504
Show file tree
Hide file tree
Showing 18 changed files with 685 additions and 153 deletions.
70 changes: 48 additions & 22 deletions js/data/bucket/symbol_bucket.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ const resolveText = require('../../symbol/resolve_text');
const mergeLines = require('../../symbol/mergelines');
const clipLine = require('../../symbol/clip_line');
const util = require('../../util/util');
const scriptDetection = require('../../util/script_detection');
const loadGeometry = require('../load_geometry');
const CollisionFeature = require('../../symbol/collision_feature');
const findPoleOfInaccessibility = require('../../util/find_pole_of_inaccessibility');
const classifyRings = require('../../util/classify_rings');

const shapeText = Shaping.shapeText;
const shapeIcon = Shaping.shapeIcon;
const WritingMode = Shaping.WritingMode;
const getGlyphQuads = Quads.getGlyphQuads;
const getIconQuads = Quads.getIconQuads;

Expand Down Expand Up @@ -271,12 +273,20 @@ class SymbolBucket {
const spacing = layout['text-letter-spacing'] * oneEm;
const textOffset = [layout['text-offset'][0] * oneEm, layout['text-offset'][1] * oneEm];
const fontstack = this.fontstack = layout['text-font'].join(',');
const textAlongLine = layout['text-rotation-alignment'] === 'map' && layout['symbol-placement'] === 'line';

for (const feature of this.features) {
let shapedText;

let shapedTextOrientations;
if (feature.text) {
shapedText = shapeText(feature.text, stacks[fontstack], maxWidth,
lineHeight, horizontalAlign, verticalAlign, justify, spacing, textOffset);
const allowsVerticalWritingMode = scriptDetection.allowsVerticalWritingMode(feature.text);

shapedTextOrientations = {
[WritingMode.horizontal]: shapeText(feature.text, stacks[fontstack], maxWidth, lineHeight, horizontalAlign, verticalAlign, justify, spacing, textOffset, oneEm, WritingMode.horizontal),
[WritingMode.vertical]: allowsVerticalWritingMode && textAlongLine && shapeText(feature.text, stacks[fontstack], maxWidth, lineHeight, horizontalAlign, verticalAlign, justify, spacing, textOffset, oneEm, WritingMode.vertical)
};
} else {
shapedTextOrientations = {};
}

let shapedIcon;
Expand All @@ -298,14 +308,14 @@ class SymbolBucket {
}
}

if (shapedText || shapedIcon) {
this.addFeature(feature, shapedText, shapedIcon);
if (shapedTextOrientations[WritingMode.horizontal] || shapedIcon) {
this.addFeature(feature, shapedTextOrientations, shapedIcon);
}
}
this.symbolInstancesEndIndex = this.symbolInstancesArray.length;
}

addFeature(feature, shapedText, shapedIcon) {
addFeature(feature, shapedTextOrientations, shapedIcon) {
const lines = feature.geometry;
const layout = this.layers[0].layout;

Expand Down Expand Up @@ -350,7 +360,7 @@ class SymbolBucket {
line,
symbolMinDistance,
textMaxAngle,
shapedText,
shapedTextOrientations[WritingMode.vertical] || shapedTextOrientations[WritingMode.horizontal],
shapedIcon,
glyphSize,
textMaxBoxScale,
Expand All @@ -369,8 +379,8 @@ class SymbolBucket {
for (let j = 0, len = anchors.length; j < len; j++) {
const anchor = anchors[j];

if (shapedText && isLine) {
if (this.anchorIsTooClose(shapedText.text, textRepeatDistance, anchor)) {
if (shapedTextOrientations[WritingMode.horizontal] && isLine) {
if (this.anchorIsTooClose(shapedTextOrientations[WritingMode.horizontal].text, textRepeatDistance, anchor)) {
continue;
}
}
Expand All @@ -389,7 +399,7 @@ class SymbolBucket {
// be drawn across tile boundaries. Instead they need to be included in
// the buffers for both tiles and clipped to tile boundaries at draw time.
const addToBuffers = inside || mayOverlap;
this.addSymbolInstance(anchor, line, shapedText, shapedIcon, this.layers[0],
this.addSymbolInstance(anchor, line, shapedTextOrientations, shapedIcon, this.layers[0],
addToBuffers, this.symbolInstancesArray.length, this.collisionBoxArray, feature.index, feature.sourceLayerIndex, this.index,
textBoxScale, textPadding, textAlongLine,
iconBoxScale, iconPadding, iconAlongLine, {zoom: this.zoom}, feature.properties);
Expand Down Expand Up @@ -516,7 +526,7 @@ class SymbolBucket {
if (hasText) {
collisionTile.insertCollisionFeature(textCollisionFeature, glyphScale, layout['text-ignore-placement']);
if (glyphScale <= maxScale) {
this.addSymbols(this.arrays.glyph, symbolInstance.glyphQuadStartIndex, symbolInstance.glyphQuadEndIndex, glyphScale, layout['text-keep-upright'], textAlongLine, collisionTile.angle);
this.addSymbols(this.arrays.glyph, symbolInstance.glyphQuadStartIndex, symbolInstance.glyphQuadEndIndex, glyphScale, layout['text-keep-upright'], textAlongLine, collisionTile.angle, symbolInstance.writingModes);
}
}

Expand All @@ -532,7 +542,7 @@ class SymbolBucket {
if (showCollisionBoxes) this.addToDebugBuffers(collisionTile);
}

addSymbols(arrays, quadsStart, quadsEnd, scale, keepUpright, alongLine, placementAngle) {
addSymbols(arrays, quadsStart, quadsEnd, scale, keepUpright, alongLine, placementAngle, writingModes) {
const elementArray = arrays.elementArray;
const layoutVertexArray = arrays.layoutVertexArray;

Expand All @@ -543,9 +553,13 @@ class SymbolBucket {

const symbol = this.symbolQuadsArray.get(k).SymbolQuad;

// drop upside down versions of glyphs
// drop incorrectly oriented glyphs
const a = (symbol.anchorAngle + placementAngle + Math.PI) % (Math.PI * 2);
if (keepUpright && alongLine && (a <= Math.PI / 2 || a > Math.PI * 3 / 2)) continue;
if (writingModes & WritingMode.vertical) {
if (alongLine && symbol.writingMode === WritingMode.vertical) {
if (keepUpright && alongLine && a <= (Math.PI * 5 / 4) || a > (Math.PI * 7 / 4)) continue;
} else if (keepUpright && alongLine && a <= (Math.PI * 3 / 4) || a > (Math.PI * 5 / 4)) continue;
} else if (keepUpright && alongLine && (a <= Math.PI / 2 || a > Math.PI * 3 / 2)) continue;

const tl = symbol.tl,
tr = symbol.tr,
Expand Down Expand Up @@ -630,14 +644,17 @@ class SymbolBucket {
}
}

addSymbolInstance(anchor, line, shapedText, shapedIcon, layer, addToBuffers, index, collisionBoxArray, featureIndex, sourceLayerIndex, bucketIndex,
addSymbolInstance(anchor, line, shapedTextOrientations, shapedIcon, layer, addToBuffers, index, collisionBoxArray, featureIndex, sourceLayerIndex, bucketIndex,
textBoxScale, textPadding, textAlongLine,
iconBoxScale, iconPadding, iconAlongLine, globalProperties, featureProperties) {

let textCollisionFeature, iconCollisionFeature, glyphQuads, iconQuads;
if (shapedText) {
glyphQuads = addToBuffers ? getGlyphQuads(anchor, shapedText, textBoxScale, line, layer, textAlongLine) : [];
textCollisionFeature = new CollisionFeature(collisionBoxArray, line, anchor, featureIndex, sourceLayerIndex, bucketIndex, shapedText, textBoxScale, textPadding, textAlongLine, false);
let textCollisionFeature, iconCollisionFeature, iconQuads;
let glyphQuads = [];
for (const writingModeString in shapedTextOrientations) {
const writingMode = parseInt(writingModeString, 10);
if (!shapedTextOrientations[writingMode]) continue;
glyphQuads = glyphQuads.concat(addToBuffers ? getGlyphQuads(anchor, shapedTextOrientations[writingMode], textBoxScale, line, layer, textAlongLine, writingMode) : []);
textCollisionFeature = new CollisionFeature(collisionBoxArray, line, anchor, featureIndex, sourceLayerIndex, bucketIndex, shapedTextOrientations[writingMode], textBoxScale, textPadding, textAlongLine, false);
}

const glyphQuadStartIndex = this.symbolQuadsArray.length;
Expand All @@ -652,7 +669,7 @@ class SymbolBucket {
const textBoxEndIndex = textCollisionFeature ? textCollisionFeature.boxEndIndex : this.collisionBoxArray.length;

if (shapedIcon) {
iconQuads = addToBuffers ? getIconQuads(anchor, shapedIcon, iconBoxScale, line, layer, iconAlongLine, shapedText, globalProperties, featureProperties) : [];
iconQuads = addToBuffers ? getIconQuads(anchor, shapedIcon, iconBoxScale, line, layer, iconAlongLine, shapedTextOrientations[WritingMode.horizontal], globalProperties, featureProperties) : [];
iconCollisionFeature = new CollisionFeature(collisionBoxArray, line, anchor, featureIndex, sourceLayerIndex, bucketIndex, shapedIcon, iconBoxScale, iconPadding, iconAlongLine, true);
}

Expand All @@ -667,6 +684,11 @@ class SymbolBucket {
if (iconQuadEndIndex > SymbolBucket.MAX_QUADS) util.warnOnce("Too many symbols being rendered in a tile. See https://github.com/mapbox/mapbox-gl-js/issues/2907");
if (glyphQuadEndIndex > SymbolBucket.MAX_QUADS) util.warnOnce("Too many glyphs being rendered in a tile. See https://github.com/mapbox/mapbox-gl-js/issues/2907");

const writingModes = (
(shapedTextOrientations[WritingMode.vertical] ? WritingMode.vertical : 0) |
(shapedTextOrientations[WritingMode.horizontal] ? WritingMode.horizontal : 0)
);

return this.symbolInstancesArray.emplaceBack(
textBoxStartIndex,
textBoxEndIndex,
Expand All @@ -678,7 +700,9 @@ class SymbolBucket {
iconQuadEndIndex,
anchor.x,
anchor.y,
index);
index,
writingModes
);
}

addSymbolQuad(symbolQuad) {
Expand All @@ -705,7 +729,9 @@ class SymbolBucket {
symbolQuad.glyphAngle,
// scales
symbolQuad.maxScale,
symbolQuad.minScale);
symbolQuad.minScale,
// writing mode
symbolQuad.writingMode);
}
}

Expand Down
16 changes: 12 additions & 4 deletions js/symbol/glyph_source.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const normalizeURL = require('../util/mapbox').normalizeGlyphsURL;
const ajax = require('../util/ajax');
const verticalizePunctuation = require('../util/verticalize_punctuation');
const Glyphs = require('../util/glyphs');
const GlyphAtlas = require('../symbol/glyph_atlas');
const Protobuf = require('pbf');
Expand Down Expand Up @@ -52,11 +53,9 @@ class GlyphSource {

const missing = {};
let remaining = 0;
let range;

for (let i = 0; i < glyphIDs.length; i++) {
const glyphID = glyphIDs[i];
range = Math.floor(glyphID / 256);
const getGlyph = (glyphID) => {
const range = Math.floor(glyphID / 256);

if (stack[range]) {
const glyph = stack[range].glyphs[glyphID];
Expand All @@ -69,6 +68,15 @@ class GlyphSource {
}
missing[range].push(glyphID);
}
};

for (let i = 0; i < glyphIDs.length; i++) {
const glyphID = glyphIDs[i];
const string = String.fromCodePoint(glyphID);
getGlyph(glyphID);
if (verticalizePunctuation.lookup[string]) {
getGlyph(verticalizePunctuation.lookup[string].codePointAt(0));
}
}

if (!remaining) callback(undefined, glyphs, fontstack);
Expand Down
30 changes: 20 additions & 10 deletions js/symbol/quads.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const minScale = 0.5; // underscale by 1 zoom level
* @class SymbolQuad
* @private
*/
function SymbolQuad(anchorPoint, tl, tr, bl, br, tex, anchorAngle, glyphAngle, minScale, maxScale) {
function SymbolQuad(anchorPoint, tl, tr, bl, br, tex, anchorAngle, glyphAngle, minScale, maxScale, writingMode) {
this.anchorPoint = anchorPoint;
this.tl = tl;
this.tr = tr;
Expand All @@ -40,6 +40,7 @@ function SymbolQuad(anchorPoint, tl, tr, bl, br, tex, anchorAngle, glyphAngle, m
this.glyphAngle = glyphAngle;
this.minScale = minScale;
this.maxScale = maxScale;
this.writingMode = writingMode;
}

/**
Expand Down Expand Up @@ -171,15 +172,24 @@ function getGlyphQuads(anchor, shaping, boxScale, line, layer, alongLine) {
}];
}

const x1 = positionedGlyph.x + glyph.left,
y1 = positionedGlyph.y - glyph.top,
x2 = x1 + rect.w,
y2 = y1 + rect.h,
const x1 = positionedGlyph.x + glyph.left;
const y1 = positionedGlyph.y - glyph.top;
const x2 = x1 + rect.w;
const y2 = y1 + rect.h;

otl = new Point(x1, y1),
otr = new Point(x2, y1),
obl = new Point(x1, y2),
obr = new Point(x2, y2);
const center = new Point(positionedGlyph.x, glyph.advance / 2);

const otl = new Point(x1, y1);
const otr = new Point(x2, y1);
const obl = new Point(x1, y2);
const obr = new Point(x2, y2);

if (positionedGlyph.angle !== 0) {
otl._sub(center)._rotate(positionedGlyph.angle)._add(center);
otr._sub(center)._rotate(positionedGlyph.angle)._add(center);
obl._sub(center)._rotate(positionedGlyph.angle)._add(center);
obr._sub(center)._rotate(positionedGlyph.angle)._add(center);
}

for (let i = 0; i < glyphInstances.length; i++) {

Expand All @@ -205,7 +215,7 @@ function getGlyphQuads(anchor, shaping, boxScale, line, layer, alongLine) {

const anchorAngle = (anchor.angle + instance.offset + 2 * Math.PI) % (2 * Math.PI);
const glyphAngle = (instance.angle + instance.offset + 2 * Math.PI) % (2 * Math.PI);
quads.push(new SymbolQuad(instance.anchorPoint, tl, tr, bl, br, rect, anchorAngle, glyphAngle, glyphMinScale, instance.maxScale));
quads.push(new SymbolQuad(instance.anchorPoint, tl, tr, bl, br, rect, anchorAngle, glyphAngle, glyphMinScale, instance.maxScale, shaping.writingMode));
}
}

Expand Down
41 changes: 27 additions & 14 deletions js/symbol/shaping.js
Original file line number Diff line number Diff line change
@@ -1,62 +1,75 @@
'use strict';

const scriptDetection = require('../util/script_detection');
const verticalizePunctuation = require('../util/verticalize_punctuation');


const WritingMode = {
horizontal: 1,
vertical: 2
};

module.exports = {
shapeText: shapeText,
shapeIcon: shapeIcon
shapeIcon: shapeIcon,
WritingMode: WritingMode
};


// The position of a glyph relative to the text's anchor point.
function PositionedGlyph(codePoint, x, y, glyph) {
function PositionedGlyph(codePoint, x, y, glyph, angle) {
this.codePoint = codePoint;
this.x = x;
this.y = y;
this.glyph = glyph || null;
this.angle = angle;
}

// A collection of positioned glyphs and some metadata
function Shaping(positionedGlyphs, text, top, bottom, left, right) {
function Shaping(positionedGlyphs, text, top, bottom, left, right, writingMode) {
this.positionedGlyphs = positionedGlyphs;
this.text = text;
this.top = top;
this.bottom = bottom;
this.left = left;
this.right = right;
this.writingMode = writingMode;
}

const newLine = 0x0a;

function shapeText(text, glyphs, maxWidth, lineHeight, horizontalAlign, verticalAlign, justify, spacing, translate) {
function shapeText(text, glyphs, maxWidth, lineHeight, horizontalAlign, verticalAlign, justify, spacing, translate, verticalHeight, writingMode) {

text = text.trim();
if (writingMode === WritingMode.vertical) text = verticalizePunctuation(text);

const positionedGlyphs = [];
const shaping = new Shaping(positionedGlyphs, text, translate[1], translate[1], translate[0], translate[0]);
const shaping = new Shaping(positionedGlyphs, text, translate[1], translate[1], translate[0], translate[0], writingMode);

// the y offset *should* be part of the font metadata
const yOffset = -17;

let x = 0;
const y = yOffset;

text = text.trim();

for (let i = 0; i < text.length; i++) {
const codePoint = text.charCodeAt(i);
const glyph = glyphs[codePoint];

if (!glyph && codePoint !== newLine) continue;

positionedGlyphs.push(new PositionedGlyph(codePoint, x, y, glyph));
if (!scriptDetection.charAllowsVerticalWritingMode(codePoint) || writingMode === WritingMode.horizontal) {
positionedGlyphs.push(new PositionedGlyph(codePoint, x, yOffset, glyph, 0));
if (glyph) x += glyph.advance + spacing;

if (glyph) {
x += glyph.advance + spacing;
} else {
positionedGlyphs.push(new PositionedGlyph(codePoint, x, 0, glyph, -Math.PI / 2));
if (glyph) x += verticalHeight + spacing;
}
}

if (!positionedGlyphs.length) return false;

linewrap(shaping, glyphs, lineHeight, maxWidth, horizontalAlign, verticalAlign, justify, translate, scriptDetection.allowsIdeographicBreaking(text));
linewrap(shaping, glyphs, lineHeight, maxWidth, horizontalAlign, verticalAlign, justify, translate, scriptDetection.allowsIdeographicBreaking(text), writingMode);

return shaping;
}
Expand All @@ -81,7 +94,7 @@ const breakable = {

invisible[newLine] = breakable[newLine] = true;

function linewrap(shaping, glyphs, lineHeight, maxWidth, horizontalAlign, verticalAlign, justify, translate, useBalancedIdeographicBreaking) {
function linewrap(shaping, glyphs, lineHeight, maxWidth, horizontalAlign, verticalAlign, justify, translate, useBalancedIdeographicBreaking, writingMode) {
let lastSafeBreak = null;
let lengthBeforeCurrentLine = 0;
let lineStartIndex = 0;
Expand All @@ -91,7 +104,7 @@ function linewrap(shaping, glyphs, lineHeight, maxWidth, horizontalAlign, vertic

const positionedGlyphs = shaping.positionedGlyphs;

if (maxWidth) {
if (writingMode === WritingMode.horizontal && maxWidth) {
if (useBalancedIdeographicBreaking) {
const lastPositionedGlyph = positionedGlyphs[positionedGlyphs.length - 1];
const estimatedLineCount = Math.max(1, Math.ceil(lastPositionedGlyph.x / maxWidth));
Expand Down
Loading

0 comments on commit 810e504

Please sign in to comment.