diff --git a/src/symbol/shaping.ts b/src/symbol/shaping.ts index 6348fbc69f..2669ca1b87 100644 --- a/src/symbol/shaping.ts +++ b/src/symbol/shaping.ts @@ -81,23 +81,28 @@ class SectionOptions { fontStack: string; // Image options imageName: string | null; + // Common options + verticalAlign: 'baseline' | 'top' | 'center'; constructor() { this.scale = 1.0; this.fontStack = ''; this.imageName = null; + this.verticalAlign = 'baseline'; } - static forText(scale: number | null, fontStack: string) { + static forText(scale: number | null, fontStack: string, verticalAlign: 'baseline' | 'top' | 'center' | null) { const textOptions = new SectionOptions(); textOptions.scale = scale || 1; textOptions.fontStack = fontStack; + textOptions.verticalAlign = verticalAlign || 'baseline'; return textOptions; } - static forImage(imageName: string) { + static forImage(imageName: string, verticalAlign: 'baseline' | 'top' | 'center' | null) { const imageOptions = new SectionOptions(); imageOptions.imageName = imageName; + imageOptions.verticalAlign = verticalAlign || 'baseline'; return imageOptions; } @@ -184,7 +189,7 @@ class TaggedString { addTextSection(section: FormattedSection, defaultFontStack: string) { this.text += section.text; - this.sections.push(SectionOptions.forText(section.scale, section.fontStack || defaultFontStack)); + this.sections.push(SectionOptions.forText(section.scale, section.fontStack || defaultFontStack, section.verticalAlign)); const index = this.sections.length - 1; for (let i = 0; i < section.text.length; ++i) { this.sectionIndex.push(index); @@ -205,7 +210,7 @@ class TaggedString { } this.text += String.fromCharCode(nextImageSectionCharCode); - this.sections.push(SectionOptions.forImage(imageName)); + this.sections.push(SectionOptions.forImage(imageName, section.verticalAlign)); this.sectionIndex.push(this.sections.length - 1); } @@ -637,7 +642,7 @@ function shapeLines(shaping: Shaping, const section = line.getSection(i); const sectionIndex = line.getSectionIndex(i); const codePoint = line.getCharCode(i); - let baselineOffset = 0.0; + let verticalAlignOffset = 0.0; let metrics = null; let rect = null; let imageName = null; @@ -665,7 +670,17 @@ function shapeLines(shaping: Shaping, // We don't know the baseline, but since we're laying out // at 24 points, we can calculate how much it will move when // we scale up or down. - baselineOffset = (lineMaxScale - section.scale) * ONE_EM; + verticalAlignOffset = (lineMaxScale - section.scale) * ONE_EM; + + // Do not offset vertical alignment for vertical text. + if (writingMode !== WritingMode.vertical) { + if (section.verticalAlign === 'top') { + verticalAlignOffset = 0; + } else if (section.verticalAlign === 'center') { + // Calculate center as the middle between top and baseline alignment. + verticalAlignOffset = (lineMaxScale - section.scale) * (ONE_EM * 2 / 3) - (ONE_EM / 6); + } + } } else { const imagePosition = imagePositions[section.imageName]; if (!imagePosition) continue; @@ -687,7 +702,19 @@ function shapeLines(shaping: Shaping, // Difference between one EM and an image size. // Aligns bottom of an image to a baseline level. const imageOffset = ONE_EM - size[1] * section.scale; - baselineOffset = maxLineOffset + imageOffset; + verticalAlignOffset = maxLineOffset + imageOffset; + + // Do not offset vertical alignment for vertical text. + if (writingMode !== WritingMode.vertical) { + if (section.verticalAlign === 'top') { + // Aligns top of an image to top of a line. + verticalAlignOffset = 0; + } else if (section.verticalAlign === 'center') { + // We calculate center as the middle between top and baseline alignment. + verticalAlignOffset = (maxLineOffset + imageOffset) / 2; + } + } + verticalAdvance = metrics.advance; // Difference between height of an image and one EM at max line scale. @@ -700,11 +727,11 @@ function shapeLines(shaping: Shaping, } if (!vertical) { - positionedGlyphs.push({glyph: codePoint, imageName, x, y: y + baselineOffset, vertical, scale: section.scale, fontStack: section.fontStack, sectionIndex, metrics, rect}); + positionedGlyphs.push({glyph: codePoint, imageName, x, y: y + verticalAlignOffset, vertical, scale: section.scale, fontStack: section.fontStack, sectionIndex, metrics, rect}); x += metrics.advance * section.scale + spacing; } else { shaping.verticalizable = true; - positionedGlyphs.push({glyph: codePoint, imageName, x, y: y + baselineOffset, vertical, scale: section.scale, fontStack: section.fontStack, sectionIndex, metrics, rect}); + positionedGlyphs.push({glyph: codePoint, imageName, x, y: y + verticalAlignOffset, vertical, scale: section.scale, fontStack: section.fontStack, sectionIndex, metrics, rect}); x += verticalAdvance * section.scale + spacing; } } diff --git a/test/integration/render/tests/text-field/formatted-vertical-align-line/expected.png b/test/integration/render/tests/text-field/formatted-vertical-align-line/expected.png new file mode 100644 index 0000000000..9b00fff230 Binary files /dev/null and b/test/integration/render/tests/text-field/formatted-vertical-align-line/expected.png differ diff --git a/test/integration/render/tests/text-field/formatted-vertical-align-line/style.json b/test/integration/render/tests/text-field/formatted-vertical-align-line/style.json new file mode 100644 index 0000000000..cfc2f6319f --- /dev/null +++ b/test/integration/render/tests/text-field/formatted-vertical-align-line/style.json @@ -0,0 +1,452 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 512, + "width": 512 + } + }, + "center": [ 0, 0 ], + "zoom": 1, + "sources": { + "line-top": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -63, + 41 + ], + [ + -57, + 44 + ], + [ + -51, + 45.8 + ], + [ + -45, + 47 + ], + [ + -39, + 48.2 + ], + [ + -33, + 49.1 + ], + [ + -27, + 49.7 + ], + [ + -21, + 50 + ], + [ + -15, + 50.3 + ], + [ + -9, + 50.9 + ], + [ + -3, + 51.8 + ], + [ + 3, + 53 + ], + [ + 9, + 54.2 + ], + [ + 15, + 56 + ], + [ + 21, + 57 + ], + [ + 27, + 58 + ], + [ + 33, + 58 + ], + [ + 39, + 59 + ], + [ + 45, + 59 + ], + [ + 51, + 58 + ], + [ + 57, + 57 + ], + [ + 63, + 55 + ] + ] + }, + "properties": {} + } + ] + } + }, + "line-center": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -63, + -9 + ], + [ + -57, + -6 + ], + [ + -51, + -4.2 + ], + [ + -45, + -3 + ], + [ + -39, + -1.8 + ], + [ + -33, + -0.9 + ], + [ + -27, + -0.3 + ], + [ + -21, + 0 + ], + [ + -15, + 0.3 + ], + [ + -9, + 0.9 + ], + [ + -3, + 1.8 + ], + [ + 3, + 3 + ], + [ + 9, + 4.2 + ], + [ + 15, + 6 + ], + [ + 21, + 7 + ], + [ + 27, + 8 + ], + [ + 33, + 8 + ], + [ + 39, + 9 + ], + [ + 45, + 9 + ], + [ + 51, + 8 + ], + [ + 57, + 7 + ], + [ + 63, + 5 + ] + ] + }, + "properties": {} + } + ] + } + }, + "line-baseline": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -63, + -59 + ], + [ + -57, + -56 + ], + [ + -51, + -54.2 + ], + [ + -45, + -53 + ], + [ + -39, + -51.8 + ], + [ + -33, + -50.9 + ], + [ + -27, + -50.3 + ], + [ + -21, + -50 + ], + [ + -15, + -49.7 + ], + [ + -9, + -49.1 + ], + [ + -3, + -48.2 + ], + [ + 3, + -47 + ], + [ + 9, + -45.8 + ], + [ + 15, + -44 + ], + [ + 21, + -43 + ], + [ + 27, + -42 + ], + [ + 33, + -42 + ], + [ + 39, + -41 + ], + [ + 45, + -41 + ], + [ + 51, + -42 + ], + [ + 57, + -43 + ], + [ + 63, + -45 + ] + ] + }, + "properties": {} + } + ] + } + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "sprite": "local://sprites/emerald", + "layers": [ + { + "id": "line-top", + "type": "line", + "source": "line-top", + "layout": {}, + "paint": { + "line-color": "#000000", + "line-width": 2 + } + }, + { + "id": "line-center", + "type": "line", + "source": "line-center", + "layout": {}, + "paint": { + "line-color": "#000000", + "line-width": 2 + } + }, + { + "id": "line-baseline", + "type": "line", + "source": "line-baseline", + "layout": {}, + "paint": { + "line-color": "#000000", + "line-width": 2 + } + }, + { + "id": "line-label-top", + "type": "symbol", + "source": "line-top", + "layout": { + "text-field": [ + "format", + ["image", "government_icon"], + { "vertical-align": "top" }, + " ", + {}, + "Ag", + { "font-scale": 1.2, "vertical-align": "top" }, + " ", + {}, + "Top", + { "font-scale": 0.8, "vertical-align": "top" } + ], + "text-size": 48, + "symbol-placement": "line-center", + "symbol-spacing": 100, + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ] + }, + "paint": { + "text-color": "#000000" + } + }, + { + "id": "line-label-center", + "type": "symbol", + "source": "line-center", + "layout": { + "text-field": [ + "format", + ["image", "government_icon"], + { "vertical-align": "center" }, + " ", + {}, + "Ag", + { "font-scale": 1.2, "vertical-align": "center" }, + " ", + {}, + "Center", + { "font-scale": 0.8, "vertical-align": "center" } + ], + "text-size": 48, + "symbol-placement": "line-center", + "symbol-spacing": 100, + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ] + }, + "paint": { + "text-color": "#000000" + } + }, + { + "id": "line-label-baseline", + "type": "symbol", + "source": "line-baseline", + "layout": { + "text-field": [ + "format", + ["image", "government_icon"], + { "vertical-align": "baseline" }, + " ", + {}, + "Ag", + { "font-scale": 1.2, "vertical-align": "baseline" }, + " ", + {}, + "Baseline", + { "font-scale": 0.8, "vertical-align": "baseline" } + ], + "text-size": 48, + "symbol-placement": "line-center", + "symbol-spacing": 100, + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ] + }, + "paint": { + "text-color": "#000000" + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render/tests/text-field/formatted-vertical-align-vertical-text/expected.png b/test/integration/render/tests/text-field/formatted-vertical-align-vertical-text/expected.png new file mode 100644 index 0000000000..3355890fe0 Binary files /dev/null and b/test/integration/render/tests/text-field/formatted-vertical-align-vertical-text/expected.png differ diff --git a/test/integration/render/tests/text-field/formatted-vertical-align-vertical-text/style.json b/test/integration/render/tests/text-field/formatted-vertical-align-vertical-text/style.json new file mode 100644 index 0000000000..af4e8491b7 --- /dev/null +++ b/test/integration/render/tests/text-field/formatted-vertical-align-vertical-text/style.json @@ -0,0 +1,45 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 512, + "width": 512 + } + }, + "center": [ 0, 0 ], + "zoom": 0, + "sources": { + "point": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [0, 0] + } + } + ] + } + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "sprite": "local://sprites/emerald", + "layers": [ + { + "id": "vertical", + "type": "symbol", + "source": "point", + "layout": { + "text-writing-mode": ["vertical"], + "text-field": ["format", "H", { "vertical-align": "top" }, "H", { "vertical-align": "center" }, "H", ["image", "interstate_1"], { "vertical-align": "center" }, ["image", "government_icon"], { "vertical-align": "top" }, ["image", "government_icon"], { "vertical-align": "center" }, ["image", "government_icon"], "ッ",{"font-scale": 1.8}], + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ] + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render/tests/text-field/formatted-vertical-align/expected.png b/test/integration/render/tests/text-field/formatted-vertical-align/expected.png new file mode 100644 index 0000000000..6041666a95 Binary files /dev/null and b/test/integration/render/tests/text-field/formatted-vertical-align/expected.png differ diff --git a/test/integration/render/tests/text-field/formatted-vertical-align/style.json b/test/integration/render/tests/text-field/formatted-vertical-align/style.json new file mode 100644 index 0000000000..75439c8b6a --- /dev/null +++ b/test/integration/render/tests/text-field/formatted-vertical-align/style.json @@ -0,0 +1,119 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 512, + "width": 512 + } + }, + "center": [ 0, 0 ], + "zoom": 0, + "sources": { + "point": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [0, 0] + } + } + ] + } + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "sprite": "local://sprites/emerald", + "layers": [ + { + "id": "point-label-top", + "type": "symbol", + "source": "point", + "layout": { + "text-field": [ + "format", + ["image", "government_icon"], + { "vertical-align": "top" }, + " ", + {}, + "ÓÑt yg", + { "font-scale": 1.2, "vertical-align": "top" }, + " ", + {}, + "TOP", + { "font-scale": 0.8, "vertical-align": "top" } + ], + "text-size": 48, + "text-offset": [0, -2], + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ] + }, + "paint": { + "text-color": "#000000" + } + }, + { + "id": "point-label-center", + "type": "symbol", + "source": "point", + "layout": { + "text-field": [ + "format", + ["image", "government_icon"], + { "vertical-align": "center" }, + " ", + {}, + "ÓÑt yg", + { "font-scale": 1.2, "vertical-align": "center" }, + " ", + {}, + "CENTER", + { "font-scale": 0.8, "vertical-align": "center" } + ], + "text-size": 48, + "text-offset": [0, 0], + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ] + }, + "paint": { + "text-color": "#000000" + } + }, + { + "id": "point-label-baseline", + "type": "symbol", + "source": "point", + "layout": { + "text-field": [ + "format", + ["image", "government_icon"], + { "vertical-align": "baseline" }, + " ", + {}, + "ÓÑt yg", + { "font-scale": 1.2, "vertical-align": "baseline" }, + " ", + {}, + "BASELINE", + { "font-scale": 0.8, "vertical-align": "baseline" } + ], + "text-size": 48, + "text-offset": [0, 2], + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ] + }, + "paint": { + "text-color": "#000000" + } + } + ] +} \ No newline at end of file diff --git a/test/integration/symbol-shaping/shaping.test.ts b/test/integration/symbol-shaping/shaping.test.ts index fca54cd0fd..6130cc009a 100644 --- a/test/integration/symbol-shaping/shaping.test.ts +++ b/test/integration/symbol-shaping/shaping.test.ts @@ -22,11 +22,11 @@ if (typeof process !== 'undefined' && process.env !== undefined) { } function sectionForImage(name: string) { - return new FormattedSection('', ResolvedImage.fromString(name), null, null, null); + return new FormattedSection('', ResolvedImage.fromString(name), null, null, null, null); } function sectionForText(name: string, scale?: number) { - return new FormattedSection(name, null, scale, null, null); + return new FormattedSection(name, null, scale, null, null, null); } describe('shaping', () => {