Cleanly separate text building and execution

This commit is contained in:
Guillaume Beraudo
2018-11-15 11:15:02 +01:00
parent afc946b215
commit 4ba84d7926
2 changed files with 129 additions and 129 deletions

View File

@@ -10,7 +10,7 @@ import {drawTextOnPath} from '../../geom/flat/textpath.js';
import {transform2D} from '../../geom/flat/transform.js';
import {CANVAS_LINE_DASH} from '../../has.js';
import {isEmpty} from '../../obj.js';
import {drawImage, resetTransform, defaultPadding} from '../canvas.js';
import {drawImage, resetTransform, defaultPadding, defaultTextBaseline} from '../canvas.js';
import CanvasInstruction from './Instruction.js';
import {TEXT_ALIGN} from '../replay.js';
import {
@@ -525,6 +525,35 @@ class CanvasInstructionsExecutor {
}
}
/**
* @private
* @param {string} text The text to draw.
* @param {string} textKey The key of the text state.
* @param {string} strokeKey The key for the stroke state.
* @param {string} fillKey The key for the fill state.
* @return {{label: HTMLCanvasElement, anchorX: number, anchorY: number}} The text image and its anchor.
*/
drawTextImageWithPointPlacement_(text, textKey, strokeKey, fillKey) {
const textState = this.textStates[textKey];
const label = this.getImage(text, textKey, fillKey, strokeKey);
const strokeState = this.strokeStates[strokeKey]; // FIXME: check if it is correct, was this.textStrokeState_;
const pixelRatio = this.pixelRatio;
const align = TEXT_ALIGN[textState.textAlign || defaultTextAlign];
const baseline = TEXT_ALIGN[textState.textBaseline || defaultTextBaseline]; // FIXME: why I need a default now?
const strokeWidth = strokeState && strokeState.lineWidth ? strokeState.lineWidth : 0;
const anchorX = align * label.width / pixelRatio + 2 * (0.5 - align) * strokeWidth;
const anchorY = baseline * label.height / pixelRatio + 2 * (0.5 - baseline) * strokeWidth;
return {
label: label,
anchorX: anchorX,
anchorY: anchorY
};
}
/**
* @private
* @param {CanvasRenderingContext2D} context Context.
@@ -566,7 +595,8 @@ class CanvasInstructionsExecutor {
const ii = instructions.length; // end of instructions
let d = 0; // data index
let dd; // end of per-instruction data
let anchorX, anchorY, prevX, prevY, roundX, roundY, declutterGroup, image;
let anchorX, anchorY, prevX, prevY, roundX, roundY, declutterGroup, image, text, textKey;
let strokeKey, fillKey;
let pendingFill = 0;
let pendingStroke = 0;
let lastFillInstruction = null;
@@ -658,20 +688,44 @@ class CanvasInstructionsExecutor {
case CanvasInstruction.DRAW_IMAGE:
d = /** @type {number} */ (instruction[1]);
dd = /** @type {number} */ (instruction[2]);
image = /** @type {HTMLCanvasElement|HTMLVideoElement|HTMLImageElement} */
(instruction[3]);
image = /** @type {HTMLCanvasElement|HTMLVideoElement|HTMLImageElement} */ (instruction[3]);
// Remaining arguments in DRAW_IMAGE are in alphabetical order
anchorX = /** @type {number} */ (instruction[4]);
anchorY = /** @type {number} */ (instruction[5]);
declutterGroup = featureCallback ? null : /** @type {import("../canvas.js").DeclutterGroup} */ (instruction[6]);
const height = /** @type {number} */ (instruction[7]);
let height = /** @type {number} */ (instruction[7]);
const opacity = /** @type {number} */ (instruction[8]);
const originX = /** @type {number} */ (instruction[9]);
const originY = /** @type {number} */ (instruction[10]);
const rotateWithView = /** @type {boolean} */ (instruction[11]);
let rotation = /** @type {number} */ (instruction[12]);
const scale = /** @type {number} */ (instruction[13]);
const width = /** @type {number} */ (instruction[14]);
let width = /** @type {number} */ (instruction[14]);
if (!image) {
if (instruction.length < 19 || !instruction[18]) {
continue;
}
text = /** @type {string} */ (instruction[18]);
textKey = /** @type {string} */ (instruction[19]);
strokeKey = /** @type {string} */ (instruction[20]);
fillKey = /** @type {string} */ (instruction[21]);
const labelWithAnchor = this.drawTextImageWithPointPlacement_(text, textKey, strokeKey, fillKey);
const textOffsetX = /** @type {number} */ (instruction[22]);
const textOffsetY = /** @type {number} */ (instruction[23]);
image = instruction[3] = labelWithAnchor.label;
anchorX = instruction[4] = (labelWithAnchor.anchorX - textOffsetX) * this.pixelRatio;
anchorY = instruction[5] = (labelWithAnchor.anchorY - textOffsetY) * this.pixelRatio;
height = instruction[8] = image.height;
width = instruction[14] = image.width;
}
let geometryWidths;
if (instruction.length > 24) {
geometryWidths = /** @type {number} */ (instruction[24]);
}
let padding, backgroundFill, backgroundStroke;
if (instruction.length > 16) {
@@ -686,7 +740,13 @@ class CanvasInstructionsExecutor {
if (rotateWithView) {
rotation += viewRotation;
}
let widthIndex = 0;
for (; d < dd; d += 2) {
if (geometryWidths) {
if (geometryWidths[widthIndex++] < width) {
continue;
}
}
this.replayImage_(context,
pixelCoordinates[d], pixelCoordinates[d + 1], image, anchorX, anchorY,
declutterGroup, height, opacity, originX, originY, rotation, scale,
@@ -703,14 +763,14 @@ class CanvasInstructionsExecutor {
const baseline = /** @type {number} */ (instruction[3]);
declutterGroup = featureCallback ? null : /** @type {import("../canvas.js").DeclutterGroup} */ (instruction[4]);
const overflow = /** @type {number} */ (instruction[5]);
const fillKey = /** @type {string} */ (instruction[6]);
fillKey = /** @type {string} */ (instruction[6]);
const maxAngle = /** @type {number} */ (instruction[7]);
const measurePixelRatio = /** @type {number} */ (instruction[8]);
const offsetY = /** @type {number} */ (instruction[9]);
const strokeKey = /** @type {string} */ (instruction[10]);
strokeKey = /** @type {string} */ (instruction[10]);
const strokeWidth = /** @type {number} */ (instruction[11]);
const text = /** @type {string} */ (instruction[12]);
const textKey = /** @type {string} */ (instruction[13]);
text = /** @type {string} */ (instruction[12]);
textKey = /** @type {string} */ (instruction[13]);
const pixelRatioScale = /** @type {number} */ (instruction[14]);
const textState = this.textStates[textKey];

View File

@@ -3,12 +3,10 @@
*/
import {getUid} from '../../util.js';
import {asColorLike} from '../../colorlike.js';
import {createCanvasContext2D} from '../../dom.js';
import {intersects} from '../../extent.js';
import {matchingChunk} from '../../geom/flat/straightchunk.js';
import GeometryType from '../../geom/GeometryType.js';
import {CANVAS_LINE_DASH} from '../../has.js';
import {labelCache, measureTextWidths, defaultTextAlign, measureTextHeight, defaultPadding, defaultLineCap, defaultLineDashOffset, defaultLineDash, defaultLineJoin, defaultFillStyle, checkFont, defaultFont, defaultLineWidth, defaultMiterLimit, defaultStrokeStyle, defaultTextBaseline} from '../canvas.js';
import {labelCache, defaultTextAlign, defaultPadding, defaultLineCap, defaultLineDashOffset, defaultLineDash, defaultLineJoin, defaultFillStyle, checkFont, defaultFont, defaultLineWidth, defaultMiterLimit, defaultStrokeStyle, defaultTextBaseline} from '../canvas.js';
import CanvasInstruction from './Instruction.js';
import CanvasInstructionsBuilder from './InstructionsBuilder.js';
import {TEXT_ALIGN} from '../replay.js';
@@ -195,8 +193,8 @@ class CanvasTextBuilder extends CanvasInstructionsBuilder {
this.endGeometry(geometry, feature);
} else {
const label = this.getImage(this.text_, this.textKey_, this.fillKey_, this.strokeKey_);
const width = label.width / this.pixelRatio;
const geometryWidths = [];
switch (geometryType) {
case GeometryType.POINT:
case GeometryType.MULTI_POINT:
@@ -215,8 +213,8 @@ class CanvasTextBuilder extends CanvasInstructionsBuilder {
break;
case GeometryType.POLYGON:
flatCoordinates = /** @type {import("../../geom/Polygon.js").default} */ (geometry).getFlatInteriorPoint();
if (!textState.overflow && flatCoordinates[2] / this.resolution < width) {
return;
if (!textState.overflow) {
geometryWidths.push(flatCoordinates[2] / this.resolution);
}
stride = 3;
break;
@@ -224,9 +222,10 @@ class CanvasTextBuilder extends CanvasInstructionsBuilder {
const interiorPoints = /** @type {import("../../geom/MultiPolygon.js").default} */ (geometry).getFlatInteriorPoints();
flatCoordinates = [];
for (i = 0, ii = interiorPoints.length; i < ii; i += 3) {
if (textState.overflow || interiorPoints[i + 2] / this.resolution >= width) {
flatCoordinates.push(interiorPoints[i], interiorPoints[i + 1]);
if (!textState.overflow) {
geometryWidths.push(interiorPoints[i + 2] / this.resolution);
}
flatCoordinates.push(interiorPoints[i], interiorPoints[i + 1]);
}
end = flatCoordinates.length;
if (end == 0) {
@@ -236,6 +235,9 @@ class CanvasTextBuilder extends CanvasInstructionsBuilder {
default:
}
end = this.appendFlatCoordinates(flatCoordinates, 0, end, stride, false, false);
this.saveTextStates_();
if (textState.backgroundFill || textState.backgroundStroke) {
this.setFillStrokeStyle(textState.backgroundFill, textState.backgroundStroke);
if (textState.backgroundFill) {
@@ -247,122 +249,42 @@ class CanvasTextBuilder extends CanvasInstructionsBuilder {
this.hitDetectionInstructions.push(this.createStroke(this.state));
}
}
this.beginGeometry(geometry, feature);
this.drawTextImage_(label, begin, end);
// The image is unknown at this stage so we pass null; it will be computed at render time.
// For clarity, we pass Infinity for numerical values that will be computed at render time.
const pixelRatio = this.pixelRatio;
this.instructions.push([CanvasInstruction.DRAW_IMAGE, begin, end,
null, Infinity, Infinity, this.declutterGroup_, Infinity, 1, 0, 0,
this.textRotateWithView_, this.textRotation_, 1, Infinity,
textState.padding == defaultPadding ?
defaultPadding : textState.padding.map(function(p) {
return p * pixelRatio;
}),
!!textState.backgroundFill, !!textState.backgroundStroke,
this.text_, this.textKey_, this.strokeKey_, this.fillKey_,
this.textOffsetX_, this.textOffsetY_,
geometryWidths.length > 0 ? geometryWidths : null
]);
this.hitDetectionInstructions.push([CanvasInstruction.DRAW_IMAGE, begin, end,
null, Infinity, Infinity, this.declutterGroup_, Infinity, 1, 0, 0,
this.textRotateWithView_, this.textRotation_, 1 / this.pixelRatio, Infinity,
textState.padding,
!!textState.backgroundFill, !!textState.backgroundStroke,
this.text_, this.textKey_, this.strokeKey_, this.fillKey_,
this.textOffsetX_, this.textOffsetY_,
geometryWidths.length > 0 ? geometryWidths : null
]);
this.endGeometry(geometry, feature);
}
}
/**
* @param {string} text Text.
* @param {string} textKey Text style key.
* @param {string} fillKey Fill style key.
* @param {string} strokeKey Stroke style key.
* @return {HTMLCanvasElement} Image.
*/
getImage(text, textKey, fillKey, strokeKey) {
let label;
const key = strokeKey + textKey + text + fillKey + this.pixelRatio;
if (!labelCache.containsKey(key)) {
const strokeState = strokeKey ? this.strokeStates[strokeKey] || this.textStrokeState_ : null;
const fillState = fillKey ? this.fillStates[fillKey] || this.textFillState_ : null;
const textState = this.textStates[textKey] || this.textState_;
const pixelRatio = this.pixelRatio;
const scale = textState.scale * pixelRatio;
const align = TEXT_ALIGN[textState.textAlign || defaultTextAlign];
const strokeWidth = strokeKey && strokeState.lineWidth ? strokeState.lineWidth : 0;
const lines = text.split('\n');
const numLines = lines.length;
const widths = [];
const width = measureTextWidths(textState.font, lines, widths);
const lineHeight = measureTextHeight(textState.font);
const height = lineHeight * numLines;
const renderWidth = (width + strokeWidth);
const context = createCanvasContext2D(
Math.ceil(renderWidth * scale),
Math.ceil((height + strokeWidth) * scale));
label = context.canvas;
labelCache.set(key, label);
if (scale != 1) {
context.scale(scale, scale);
}
context.font = textState.font;
if (strokeKey) {
context.strokeStyle = strokeState.strokeStyle;
context.lineWidth = strokeWidth;
context.lineCap = /** @type {CanvasLineCap} */ (strokeState.lineCap);
context.lineJoin = /** @type {CanvasLineJoin} */ (strokeState.lineJoin);
context.miterLimit = strokeState.miterLimit;
if (CANVAS_LINE_DASH && strokeState.lineDash.length) {
context.setLineDash(strokeState.lineDash);
context.lineDashOffset = strokeState.lineDashOffset;
}
}
if (fillKey) {
context.fillStyle = fillState.fillStyle;
}
context.textBaseline = 'middle';
context.textAlign = 'center';
const leftRight = (0.5 - align);
const x = align * label.width / scale + leftRight * strokeWidth;
let i;
if (strokeKey) {
for (i = 0; i < numLines; ++i) {
context.strokeText(lines[i], x + leftRight * widths[i], 0.5 * (strokeWidth + lineHeight) + i * lineHeight);
}
}
if (fillKey) {
for (i = 0; i < numLines; ++i) {
context.fillText(lines[i], x + leftRight * widths[i], 0.5 * (strokeWidth + lineHeight) + i * lineHeight);
}
}
}
return labelCache.get(key);
}
/**
* @private
* @param {HTMLCanvasElement} label Label.
* @param {number} begin Begin.
* @param {number} end End.
*/
drawTextImage_(label, begin, end) {
const textState = this.textState_;
const strokeState = this.textStrokeState_;
const pixelRatio = this.pixelRatio;
const align = TEXT_ALIGN[textState.textAlign || defaultTextAlign];
const baseline = TEXT_ALIGN[textState.textBaseline];
const strokeWidth = strokeState && strokeState.lineWidth ? strokeState.lineWidth : 0;
const anchorX = align * label.width / pixelRatio + 2 * (0.5 - align) * strokeWidth;
const anchorY = baseline * label.height / pixelRatio + 2 * (0.5 - baseline) * strokeWidth;
this.instructions.push([CanvasInstruction.DRAW_IMAGE, begin, end,
label, (anchorX - this.textOffsetX_) * pixelRatio, (anchorY - this.textOffsetY_) * pixelRatio,
this.declutterGroup_, label.height, 1, 0, 0, this.textRotateWithView_, this.textRotation_,
1, label.width,
textState.padding == defaultPadding ?
defaultPadding : textState.padding.map(function(p) {
return p * pixelRatio;
}),
!!textState.backgroundFill, !!textState.backgroundStroke
]);
this.hitDetectionInstructions.push([CanvasInstruction.DRAW_IMAGE, begin, end,
label, (anchorX - this.textOffsetX_) * pixelRatio, (anchorY - this.textOffsetY_) * pixelRatio,
this.declutterGroup_, label.height, 1, 0, 0, this.textRotateWithView_, this.textRotation_,
1 / pixelRatio, label.width, textState.padding,
!!textState.backgroundFill, !!textState.backgroundStroke
]);
}
/**
* @private
* @param {number} begin Begin.
* @param {number} end End.
* @param {import("../canvas.js").DeclutterGroup} declutterGroup Declutter group.
*/
drawChars_(begin, end, declutterGroup) {
saveTextStates_() {
const strokeState = this.textStrokeState_;
const textState = this.textState_;
const fillState = this.textFillState_;
@@ -382,10 +304,11 @@ class CanvasTextBuilder extends CanvasInstructionsBuilder {
}
}
const textKey = this.textKey_;
if (!(this.textKey_ in this.textStates)) {
this.textStates[this.textKey_] = /** @type {import("../canvas.js").TextState} */ ({
if (!(textKey in this.textStates)) {
this.textStates[textKey] = /** @type {import("../canvas.js").TextState} */ ({
font: textState.font,
textAlign: textState.textAlign || defaultTextAlign,
textBaseline: textState.textBaseline || defaultTextBaseline,
scale: textState.scale
});
}
@@ -397,6 +320,23 @@ class CanvasTextBuilder extends CanvasInstructionsBuilder {
});
}
}
}
/**
* @private
* @param {number} begin Begin.
* @param {number} end End.
* @param {import("../canvas.js").DeclutterGroup} declutterGroup Declutter group.
*/
drawChars_(begin, end, declutterGroup) {
const strokeState = this.textStrokeState_;
const textState = this.textState_;
const strokeKey = this.strokeKey_;
const textKey = this.textKey_;
const fillKey = this.fillKey_;
this.saveTextStates_();
const pixelRatio = this.pixelRatio;
const baseline = TEXT_ALIGN[textState.textBaseline];