From 8f51e98b92f919ba21517c04a3da85b3f747a8d2 Mon Sep 17 00:00:00 2001 From: Sven Slootweg Date: Sun, 5 Mar 2017 21:10:40 +0100 Subject: [PATCH] Fix line height implementation and add a debugging view --- src/index.js | 20 +++-- src/layout/adjust-line-heights.js | 7 +- src/layout/index.js | 130 ++++++++++++++++++++++++++---- src/render/measure-text.js | 2 +- src/render/set-text-styles.js | 8 +- src/util/last.js | 5 ++ 6 files changed, 137 insertions(+), 35 deletions(-) create mode 100644 src/util/last.js diff --git a/src/index.js b/src/index.js index 22075fb..f4c43e3 100644 --- a/src/index.js +++ b/src/index.js @@ -8,7 +8,7 @@ const setTextStyles = require("./render/set-text-styles"); const measureText = require("./render/measure-text"); function getTextProperties(item) { - return objectPick(item, ["fontSize", "fontFamily", "fontStyle", "fontWeight"]); + return objectPick(item, ["fontSize", "fontFamily", "fontStyle", "fontWeight", "fillColor", "strokeColor"]); } module.exports = function createTextShape(options) { @@ -16,9 +16,9 @@ module.exports = function createTextShape(options) { _layout: null, _lines: null, type: "text", - cacheBustingProperties: ["fillColor", "strokeColor", "strokeWidth", "text", "fontSize", "fontFamily", "fontStyle", "fontWeight"], - sizeBustingProperties: ["text", "fontSize", "fontFamily", "fontStyle", "fontWeight"], - fillColor: "red", + cacheBustingProperties: ["fillColor", "strokeColor", "strokeWidth", "text", "fontSize", "fontFamily", "fontStyle", "fontWeight", "trimVerticalWhitespace", "renderDebugLines", "renderDebugArea", "lineHeight", "tags"], + sizeBustingProperties: ["text", "fontSize", "fontFamily", "fontStyle", "fontWeight", "trimVerticalWhitespace", "renderDebugLines", "renderDebugArea", "lineHeight", "tags"], + fillColor: "black", strokeColor: "red", strokeWidth: 0, fontFamily: "sans-serif", @@ -26,17 +26,27 @@ module.exports = function createTextShape(options) { fontWeight: "normal", lineHeight: 1.16, onRender: function onRender(context) { + if (this.renderDebugArea) { + context.fillStyle = "silver"; + context.fillRect(0, 0, this._layout.width, this._layout.height); + } + this._layout.items.forEach((item) => { setTextStyles(context, item.style); context.fillText(item.text, item.x, item.y); }); + + if (this.renderDebugLines) { + this._layout.drawDebugLines(context); + } }, onRecalculateSize: function onRecalculateSize() { this._layout = layout(this.text, { classes: options.classes, defaultStyle: getTextProperties(this), lineHeight: this.lineHeight, - tags: this.tags + tags: this.tags, + trimVerticalWhitespace: this.trimVerticalWhitespace }); return { diff --git a/src/layout/adjust-line-heights.js b/src/layout/adjust-line-heights.js index 2466782..7ebb3d6 100644 --- a/src/layout/adjust-line-heights.js +++ b/src/layout/adjust-line-heights.js @@ -2,11 +2,6 @@ module.exports = function adjustLineHeights(lines, lineHeight) { return lines.map((line, i) => { - if (i !== lines.length - 1) { - return line.height * lineHeight * 1.13; - } else { - /* The last line doesn't get a lineHeight multiplier... */ - return line.height * 1.13; - } + return line.height * lineHeight * 1.13; }); }; diff --git a/src/layout/index.js b/src/layout/index.js index d2f20b3..7b9ca3c 100644 --- a/src/layout/index.js +++ b/src/layout/index.js @@ -9,11 +9,41 @@ const adjustLineHeights = require("./adjust-line-heights"); const sum = require("../util/sum"); const min = require("../util/min"); const max = require("../util/max"); +const last = require("../util/last"); + +function drawDebugLine(context, lineNumber, y, width, color = "red") { + //context.globalAlpha = 1; + + context.strokeStyle = color; + context.beginPath(); + + if (typeof width === "number") { + context.moveTo(0, y); + context.lineTo(width, y); + } else { + context.moveTo(width[0], y); + context.lineTo(width[1], y); + } + + context.stroke(); + + let boxOffsets = ["red", "green", "blue", "orange"]; + let boxOffset = boxOffsets.indexOf(color); + + context.fillStyle = color; + context.fillRect(boxOffset * 18, y + 3, 16, 16); + + context.font = "12px sans-serif"; + context.fillStyle = "white"; + context.fillText(lineNumber, 5 + boxOffset * 18, y + 15) + + //context.globalAlpha = 1; +} module.exports = function layoutFormattedText(text, options) { if (options.tags === true) { let lines = parser(text).map((lineItems) => { - let measuredItems = lineItems.map((item) => { + let measuredItems = lineItems.filter(item => item.text !== "").map((item) => { let generatedStyle = generateStyle(item, { classes: options.classes, defaultStyle: options.defaultStyle @@ -25,33 +55,61 @@ module.exports = function layoutFormattedText(text, options) { }, item); }); - return { - alignment: (lineItems.length > 0) ? findAlignment(lineItems[0]) : null, - items: measuredItems, - height: max(measuredItems.map(item => item.measurements.height)), - width: sum(measuredItems.map(item => item.measurements.width)), - minAscender: min(measuredItems.map(item => item.measurements.ascender)), - maxDescender: max(measuredItems.map(item => item.measurements.descender)) + if (measuredItems.length > 0) { + return { + alignment: (lineItems.length > 0) ? findAlignment(lineItems[0]) : null, + items: measuredItems, + height: max(measuredItems.map(item => item.measurements.height)), + width: sum(measuredItems.map(item => item.measurements.width)), + minAscender: min(measuredItems.map(item => item.measurements.ascender)), + maxDescender: max(measuredItems.map(item => item.measurements.descender)) + } + } else { + /* This line does not contain any real elements, so we'll omit it. */ + return null; } - }); + }).filter(line => line != null); let adjustedLineHeights = adjustLineHeights(lines, options.lineHeight); - let currentLineOffset = 0; + /* We start out by moving everything up half the first line's height - this + * compensates for the middle-of-the-line-oriented line spacing of the first + * line. Otherwise, everything would be shifted off the canvas.*/ + let startHeightCorrection, endHeightCorrection; + + if (options.trimVerticalWhitespace) { + startHeightCorrection = -(adjustedLineHeights[0] / 2) - (lines[0].minAscender / 2); + endHeightCorrection = -(last(adjustedLineHeights) / 2) - (last(lines).minAscender / 2) + (last(lines).maxDescender); + } else { + startHeightCorrection = 0; + endHeightCorrection = 0; + } + + let currentLineOffset = startHeightCorrection; + + let debugLineOffset = 0; let currentComponentOffset = 0; + let debugLines = []; let positionedItems = lines.reduce((items, line, i) => { + debugLines.push({color: "red", y: debugLineOffset, lineNumber: i}); + debugLines.push({color: "green", y: currentLineOffset, lineNumber: i}); + debugLines.push({color: "orange", y: currentLineOffset + adjustedLineHeights[i], lineNumber: i}); + let newItems = items.concat(line.items.map((item) => { let positionedItem = Object.assign({ x: currentComponentOffset, - y: currentLineOffset - line.minAscender + y: currentLineOffset - (line.minAscender / 2) + (adjustedLineHeights[i] / 2) }, item); + debugLines.push({color: "blue", y: positionedItem.y, x1: positionedItem.x, x2: positionedItem.x + item.measurements.width, lineNumber: i}); + currentComponentOffset += item.measurements.width; return positionedItem; })); currentLineOffset += adjustedLineHeights[i]; + debugLineOffset += adjustedLineHeights[i]; currentComponentOffset = 0; return newItems; @@ -59,10 +117,25 @@ module.exports = function layoutFormattedText(text, options) { return { width: Math.ceil(max(lines.map(line => line.width))), - height: Math.ceil(sum(adjustedLineHeights)), - items: positionedItems + height: Math.ceil(sum(adjustedLineHeights) + startHeightCorrection + endHeightCorrection), + items: positionedItems, + drawDebugLines: function drawDebugLines(context) { + debugLines.forEach((debugLine) => { + let width; + + if (debugLine.x1 != null) { + width = [debugLine.x1, debugLine.x2]; + } else { + width = this.width; + } + + drawDebugLine(context, debugLine.lineNumber, debugLine.y, width, debugLine.color); + }); + } } } else { + let debugLines = []; + let lines = text.split("\n").map((line) => { return { text: line, @@ -77,7 +150,19 @@ module.exports = function layoutFormattedText(text, options) { } }), options.lineHeight); - let currentLineOffset = 0; + let startHeightCorrection, endHeightCorrection; + + if (options.trimVerticalWhitespace) { + startHeightCorrection = -(adjustedLineHeights[0] / 2) - (lines[0].measurements.ascender / 2); + endHeightCorrection = -(last(adjustedLineHeights) / 2) - (last(lines).measurements.ascender / 2) + (last(lines).measurements.descender); + } else { + startHeightCorrection = 0; + endHeightCorrection = 0; + } + + let currentLineOffset = startHeightCorrection; + + let debugLineOffset = 0; let positionedItems = lines.map((line, i) => { let newItem = { @@ -85,18 +170,29 @@ module.exports = function layoutFormattedText(text, options) { measurements: line.measurements, style: options.defaultStyle, x: 0, - y: currentLineOffset - line.measurements.ascender + y: currentLineOffset - (line.measurements.ascender / 2) + (adjustedLineHeights[i] / 2) } + debugLines.push({color: "red", y: debugLineOffset, lineNumber: i}); + debugLines.push({color: "green", y: currentLineOffset, lineNumber: i}); + debugLines.push({color: "blue", y: newItem.y, lineNumber: i}); + debugLines.push({color: "orange", y: currentLineOffset + adjustedLineHeights[i], lineNumber: i}); + currentLineOffset += adjustedLineHeights[i]; + debugLineOffset += adjustedLineHeights[i]; return newItem; }); return { width: Math.ceil(max(lines.map(line => line.measurements.width))), - height: Math.ceil(sum(adjustedLineHeights)), - items: positionedItems + height: Math.ceil(sum(adjustedLineHeights) + startHeightCorrection + endHeightCorrection), + items: positionedItems, + drawDebugLines: function drawDebugLines(context) { + debugLines.forEach((debugLine) => { + drawDebugLine(context, debugLine.lineNumber, debugLine.y, this.width, debugLine.color); + }); + } } } }; diff --git a/src/render/measure-text.js b/src/render/measure-text.js index 353364f..f8eb57a 100644 --- a/src/render/measure-text.js +++ b/src/render/measure-text.js @@ -12,7 +12,7 @@ module.exports = function measureText(text, options) { setTextStyles(context, options); - let fontMeasurements = measureFont(options.fontFamily); + let fontMeasurements = measureFont(options.fontFamily, {fontSize: 40}); return Object.assign(context.measureText(text), { height: options.fontSize * (fontMeasurements.descender - fontMeasurements.topBounding), diff --git a/src/render/set-text-styles.js b/src/render/set-text-styles.js index 52ba590..8b11379 100644 --- a/src/render/set-text-styles.js +++ b/src/render/set-text-styles.js @@ -19,10 +19,6 @@ module.exports = function setTextStyles(context, options) { context.font = fontSegments.filter(segment => (segment != null)).join(" "); context.textBaseline = defaultValue(options.textBaseline, "alphabetic"); - - if (options.isStroke) { - context.strokeStyle = options.color; - } else { - context.fillStyle = options.color; - } + context.strokeStyle = options.strokeColor; + context.fillStyle = options.fillColor; } diff --git a/src/util/last.js b/src/util/last.js new file mode 100644 index 0000000..c8b83f4 --- /dev/null +++ b/src/util/last.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = function lastItem(array) { + return array[array.length - 1]; +};