Use glyph atlases

This commit is contained in:
GaborFarkas
2017-06-16 12:40:45 +02:00
parent a3a443324d
commit a4c421e699
4 changed files with 400 additions and 135 deletions

View File

@@ -6,6 +6,7 @@ goog.require('ol.dom');
goog.require('ol.has');
goog.require('ol.render.webgl');
goog.require('ol.render.webgl.TextureReplay');
goog.require('ol.style.AtlasManager');
goog.require('ol.webgl.Buffer');
@@ -49,12 +50,7 @@ if (ol.ENABLE_WEBGL) {
* lineWidth: number,
* miterLimit: (number|undefined),
* fillColor: (ol.ColorLike|null),
* text: (string|undefined),
* font: (string|undefined),
* textAlign: (number|undefined),
* textBaseline: (number|undefined),
* offsetX: (number|undefined),
* offsetY: (number|undefined),
* scale: (number|undefined)}}
*/
this.state_ = {
@@ -66,18 +62,51 @@ if (ol.ENABLE_WEBGL) {
lineWidth: 0,
miterLimit: undefined,
fillColor: null,
text: '',
font: undefined,
textAlign: undefined,
textBaseline: undefined,
offsetX: undefined,
offsetY: undefined,
scale: undefined
};
this.originX = 0;
/**
* @private
* @type {string}
*/
this.text_ = '';
this.originY = 0;
/**
* @private
* @type {number|undefined}
*/
this.textAlign_ = undefined;
/**
* @private
* @type {number|undefined}
*/
this.textBaseline_ = undefined;
/**
* @private
* @type {number|undefined}
*/
this.offsetX_ = undefined;
/**
* @private
* @type {number|undefined}
*/
this.offsetY_ = undefined;
/**
* @private
* @type {Object.<string, ol.WebglGlyphAtlas>}
*/
this.atlases_ = {};
/**
* @private
* @type {ol.WebglGlyphAtlas|undefined}
*/
this.currAtlas_ = undefined;
this.scale = 1;
@@ -92,85 +121,152 @@ if (ol.ENABLE_WEBGL) {
*/
ol.render.webgl.TextReplay.prototype.drawText = function(flatCoordinates, offset,
end, stride, geometry, feature) {
//For now we create one texture per feature. That is, only multiparts are grouped.
//TODO: speed up rendering with SDF, or at least glyph atlases
var state = this.state_;
if (state.text) {
if (this.text_) {
this.startIndices.push(this.indices.length);
this.startIndicesFeature.push(feature);
this.drawCoordinates(
flatCoordinates, offset, end, stride);
var glyphAtlas = this.currAtlas_;
var lines = this.text_.split('\n');
var textSize = this.getTextSize_(lines);
var i, ii, j, jj, currX, currY, charArr, charInfo;
var anchorX = Math.round(textSize[0] * this.textAlign_ + this.offsetX_);
var anchorY = Math.round(textSize[1] * this.textBaseline_ + this.offsetY_);
var lineWidth = (this.state_.lineWidth / 2) * this.state_.scale;
for (i = 0, ii = lines.length; i < ii; ++i) {
currX = 0;
currY = glyphAtlas.height * i;
charArr = lines[i].split('');
for (j = 0, jj = charArr.length; j < jj; ++j) {
charInfo = glyphAtlas.atlas.getInfo(charArr[j]);
if (charInfo) {
var image = charInfo.image;
this.anchorX = anchorX - currX;
this.anchorY = anchorY - currY;
this.originX = j === 0 ? charInfo.offsetX - lineWidth : charInfo.offsetX;
this.originY = charInfo.offsetY;
this.height = glyphAtlas.height;
this.width = j === 0 || j === charArr.length - 1 ?
glyphAtlas.width[charArr[j]] + lineWidth : glyphAtlas.width[charArr[j]];
this.imageHeight = image.height;
this.imageWidth = image.width;
var currentImage;
if (this.images_.length === 0) {
this.images_.push(image);
} else {
currentImage = this.images_[this.images_.length - 1];
if (ol.getUid(currentImage) != ol.getUid(image)) {
this.groupIndices.push(this.indices.length);
this.images_.push(image);
}
}
this.drawText_(flatCoordinates, offset, end, stride);
}
currX += this.width;
}
}
}
};
/**
* @private
* @return {HTMLCanvasElement} Text image.
* @param {Array.<string>} lines Label to draw split to lines.
* @return {Array.<number>} Size of the label in pixels.
*/
ol.render.webgl.TextReplay.prototype.createTextImage_ = function() {
var state = this.state_;
//Measure text dimensions
var mCtx = this.measureCanvas_.getContext('2d');
mCtx.font = state.font;
var lineHeight = Math.round(mCtx.measureText('M').width * 1.2 + state.lineWidth * 2);
var lines = state.text.split('\n');
//FIXME: use pixelRatio
var textHeight = Math.ceil(lineHeight * lines.length * state.scale);
this.height = textHeight;
this.imageHeight = textHeight;
this.anchorY = Math.round(textHeight * state.textBaseline + state.offsetY);
var longestLine = lines.map(function(str) {
return mCtx.measureText(str).width;
ol.render.webgl.TextReplay.prototype.getTextSize_ = function(lines) {
var self = this;
var glyphAtlas = this.currAtlas_;
var textHeight = lines.length * glyphAtlas.height;
//Split every line to an array of chars, sum up their width, and select the longest.
var textWidth = lines.map(function(str) {
var sum = 0;
var i, ii;
for (i = 0, ii = str.length; i < ii; ++i) {
var curr = str[i];
if (!glyphAtlas.width[curr]) {
self.addCharToAtlas_(curr);
}
sum += glyphAtlas.width[curr] ? glyphAtlas.width[curr] : 0;
}
return sum;
}).reduce(function(max, curr) {
return Math.max(max, curr);
});
//FIXME: use pixelRatio
var textWidth = Math.ceil((longestLine + state.lineWidth * 2) * state.scale);
this.width = textWidth;
this.imageWidth = textWidth;
this.anchorX = Math.round(textWidth * state.textAlign + state.offsetX);
//Create a canvas
var ctx = ol.dom.createCanvasContext2D(textWidth, textHeight);
var canvas = ctx.canvas;
return [textWidth, textHeight];
};
//Parameterize the canvas
ctx.font = /** @type {string} */ (state.font);
ctx.fillStyle = state.fillColor;
ctx.strokeStyle = state.strokeColor;
ctx.lineWidth = state.lineWidth;
ctx.lineCap = /*** @type {string} */ (state.lineCap);
ctx.lineJoin = /** @type {string} */ (state.lineJoin);
ctx.miterLimit = /** @type {number} */ (state.miterLimit);
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
if (ol.has.CANVAS_LINE_DASH && state.lineDash) {
//FIXME: use pixelRatio
ctx.setLineDash(state.lineDash);
ctx.lineDashOffset = /** @type {number} */ (state.lineDashOffset);
}
if (state.scale !== 1) {
//FIXME: use pixelRatio
ctx.setTransform(/** @type {number} */ (state.scale), 0, 0,
/** @type {number} */ (state.scale), 0, 0);
/**
* @private
* @param {Array.<number>} flatCoordinates Flat coordinates.
* @param {number} offset Offset.
* @param {number} end End.
* @param {number} stride Stride.
*/
ol.render.webgl.TextReplay.prototype.drawText_ = function(flatCoordinates, offset,
end, stride) {
var i, ii;
for (i = offset, ii = end; i < ii; i += stride) {
this.drawCoordinates(flatCoordinates, offset, end, stride);
}
};
//Draw the text on the canvas
var lineY = 0;
for (var i = 0, ii = lines.length; i < ii; ++i) {
if (state.strokeColor) {
ctx.strokeText(lines[i], 0, lineY);
/**
* @private
* @param {string} char Character.
*/
ol.render.webgl.TextReplay.prototype.addCharToAtlas_ = function(char) {
if (char.length === 1) {
var glyphAtlas = this.currAtlas_;
var state = this.state_;
var mCtx = this.measureCanvas_.getContext('2d');
mCtx.font = state.font;
var width = Math.ceil(mCtx.measureText(char).width * state.scale);
var info = glyphAtlas.atlas.add(char, width, glyphAtlas.height,
function(ctx, x, y) {
//Parameterize the canvas
ctx.font = /** @type {string} */ (state.font);
ctx.fillStyle = state.fillColor;
ctx.strokeStyle = state.strokeColor;
ctx.lineWidth = state.lineWidth;
ctx.lineCap = /*** @type {string} */ (state.lineCap);
ctx.lineJoin = /** @type {string} */ (state.lineJoin);
ctx.miterLimit = /** @type {number} */ (state.miterLimit);
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
if (ol.has.CANVAS_LINE_DASH && state.lineDash) {
//FIXME: use pixelRatio
ctx.setLineDash(state.lineDash);
ctx.lineDashOffset = /** @type {number} */ (state.lineDashOffset);
}
if (state.scale !== 1) {
//FIXME: use pixelRatio
ctx.setTransform(/** @type {number} */ (state.scale), 0, 0,
/** @type {number} */ (state.scale), 0, 0);
}
//Draw the character on the canvas
if (state.strokeColor) {
ctx.strokeText(char, x, y);
}
if (state.fillColor) {
ctx.fillText(char, x, y);
}
});
if (info) {
glyphAtlas.width[char] = width;
}
if (state.fillColor) {
ctx.fillText(lines[i], 0, lineY);
}
lineY += lineHeight;
}
return /** @type {HTMLCanvasElement} */ (canvas);
};
@@ -204,15 +300,17 @@ if (ol.ENABLE_WEBGL) {
lineWidth: 0,
miterLimit: undefined,
fillColor: null,
text: '',
font: undefined,
textAlign: undefined,
textBaseline: undefined,
offsetX: undefined,
offsetY: undefined,
scale: undefined
};
this.text_ = '';
this.textAlign_ = undefined;
this.textBaseline_ = undefined;
this.offsetX_ = undefined;
this.offsetY_ = undefined;
this.images_ = null;
this.atlases_ = {};
this.currAtlas_ = undefined;
ol.render.webgl.TextureReplay.prototype.finish.call(this, context);
};
@@ -225,7 +323,7 @@ if (ol.ENABLE_WEBGL) {
var textFillStyle = textStyle.getFill();
var textStrokeStyle = textStyle.getStroke();
if (!textStyle || !textStyle.getText() || (!textFillStyle && !textStrokeStyle)) {
state.text = '';
this.text_ = '';
} else {
if (!textFillStyle) {
state.fillColor = null;
@@ -251,25 +349,72 @@ if (ol.ENABLE_WEBGL) {
}
state.font = textStyle.getFont() || ol.render.webgl.defaultFont;
state.scale = textStyle.getScale() || 1;
state.text = textStyle.getText();
this.text_ = /** @type {string} */ (textStyle.getText());
var textAlign = ol.render.webgl.TextReplay.Align_[textStyle.getTextAlign()];
var textBaseline = ol.render.webgl.TextReplay.Align_[textStyle.getTextBaseline()];
state.textAlign = textAlign === undefined ?
this.textAlign_ = textAlign === undefined ?
ol.render.webgl.defaultTextAlign : textAlign;
state.textBaseline = textBaseline === undefined ?
this.textBaseline_ = textBaseline === undefined ?
ol.render.webgl.defaultTextBaseline : textBaseline;
state.offsetX = textStyle.getOffsetX() || 0;
state.offsetY = textStyle.getOffsetY() || 0;
this.offsetX_ = textStyle.getOffsetX() || 0;
this.offsetY_ = textStyle.getOffsetY() || 0;
this.rotateWithView = !!textStyle.getRotateWithView();
this.rotation = textStyle.getRotation() || 0;
if (this.images_.length === 0) {
this.images_.push(this.createTextImage_());
} else {
this.groupIndices.push(this.indices.length);
this.images_.push(this.createTextImage_());
this.currAtlas_ = this.getAtlas_(state);
}
};
/**
* @private
* @param {Object} state Font attributes.
* @return {ol.WebglGlyphAtlas} Glyph atlas.
*/
ol.render.webgl.TextReplay.prototype.getAtlas_ = function(state) {
var params = [];
var i;
for (i in state) {
if (state[i] || state[i] === 0) {
if (Array.isArray(state[i])) {
params = params.concat(state[i]);
} else {
params.push(state[i]);
}
}
}
var hash = this.calculateHash_(params);
if (!this.atlases_[hash]) {
var mCtx = this.measureCanvas_.getContext('2d');
mCtx.font = state.font;
var height = Math.ceil((mCtx.measureText('M').width * 1.5 +
state.lineWidth / 2) * state.scale);
this.atlases_[hash] = {
atlas: new ol.style.AtlasManager({
space: state.lineWidth + 1
}),
width: {},
height: height
};
}
return this.atlases_[hash];
};
/**
* @private
* @param {Array.<string|number>} params Array of parameters.
* @return {string} Hash string.
*/
ol.render.webgl.TextReplay.prototype.calculateHash_ = function(params) {
//TODO: Create a more performant, reliable, general hash function.
var i, ii;
var hash = '';
for (i = 0, ii = params.length; i < ii; ++i) {
hash += params[i];
}
return hash;
};

View File

@@ -723,6 +723,14 @@ ol.ViewAnimation;
ol.WebglBufferCacheEntry;
/**
* @typedef {{atlas: ol.style.AtlasManager,
* width: Object.<string, number>,
* height: number}}
*/
ol.WebglGlyphAtlas;
/**
* @typedef {{p0: ol.WebglPolygonVertex,
* p1: ol.WebglPolygonVertex}}

View File

@@ -75,28 +75,15 @@ describe('ol.render.webgl.TextReplay', function() {
});
it('set expected states', function() {
var mCtx = ol.dom.createCanvasContext2D(0, 0);
mCtx.font = '12px Arial';
var mWidth = mCtx.measureText('M').width;
var textWidth = mCtx.measureText('someText').width;
var width = Math.ceil((textWidth + 2) * 2);
var height = Math.ceil(Math.round(mWidth * 1.2 + 2) * 2);
replay.setTextStyle(textStyle1);
expect(replay.anchorX).to.be(10);
expect(replay.anchorY).to.be(10);
expect(replay.height).to.be(height);
expect(replay.imageHeight).to.be(height);
expect(replay.width).to.be(width);
expect(replay.imageWidth).to.be(width);
expect(replay.opacity).to.be(1);
expect(replay.originX).to.be(0);
expect(replay.originY).to.be(0);
expect(replay.rotation).to.be(1.5);
expect(replay.rotateWithView).to.be(true);
expect(replay.scale).to.be(1);
expect(replay.images_).to.have.length(1);
expect(replay.groupIndices).to.have.length(0);
expect(replay.offsetX_).to.be(10);
expect(replay.offsetY_).to.be(10);
expect(replay.text_).to.be('someText');
expect(Object.keys(replay.atlases_)).to.have.length(1);
expect(replay.state_.fillColor).to.be('rgba(0,0,0,1)');
expect(replay.state_.strokeColor).to.be('rgba(0,0,0,1)');
@@ -108,25 +95,21 @@ describe('ol.render.webgl.TextReplay', function() {
expect(replay.state_.lineDashOffset).to.be(15);
expect(replay.state_.miterLimit).to.be(2);
expect(replay.state_.font).to.be('12px Arial');
expect(replay.state_.text).to.be('someText');
replay.setTextStyle(textStyle2);
expect(replay.images_).to.have.length(2);
expect(replay.groupIndices).to.have.length(1);
expect(Object.keys(replay.atlases_)).to.have.length(2);
});
it('does not create an image, if an empty text is supplied', function() {
it('does not create an atlas, if an empty text is supplied', function() {
replay.setTextStyle(textStyle4);
expect(replay.state_.text).to.be('');
expect(replay.images_).to.have.length(0);
expect(replay.groupIndices).to.have.length(0);
expect(replay.text_).to.be('');
expect(Object.keys(replay.atlases_)).to.have.length(0);
});
it('does not create an image, if both fill and stroke styles are missing', function() {
it('does not create an atlas, if both fill and stroke styles are missing', function() {
replay.setTextStyle(textStyle3);
expect(replay.state_.text).to.be('');
expect(replay.images_).to.have.length(0);
expect(replay.groupIndices).to.have.length(0);
expect(replay.text_).to.be('');
expect(Object.keys(replay.atlases_)).to.have.length(0);
});
});
@@ -145,29 +128,38 @@ describe('ol.render.webgl.TextReplay', function() {
point = [1000, 2000];
replay.drawText(point, 0, 2, 2, null, null);
expect(replay.vertices).to.have.length(32);
expect(replay.indices).to.have.length(6);
expect(replay.indices[0]).to.be(0);
expect(replay.indices[1]).to.be(1);
expect(replay.indices[2]).to.be(2);
expect(replay.indices[3]).to.be(0);
expect(replay.indices[4]).to.be(2);
expect(replay.indices[5]).to.be(3);
expect(replay.vertices).to.have.length(256);
expect(replay.indices).to.have.length(48);
point = [2000, 3000];
replay.drawText(point, 0, 2, 2, null, null);
expect(replay.vertices).to.have.length(64);
expect(replay.indices).to.have.length(12);
expect(replay.indices[6]).to.be(4);
expect(replay.indices[7]).to.be(5);
expect(replay.indices[8]).to.be(6);
expect(replay.indices[9]).to.be(4);
expect(replay.indices[10]).to.be(6);
expect(replay.indices[11]).to.be(7);
expect(replay.vertices).to.have.length(512);
expect(replay.indices).to.have.length(96);
});
it('sets part of its state during drawing', function() {
var point = [1000, 2000];
replay.drawText(point, 0, 2, 2, null, null);
var height = replay.currAtlas_.height;
var widths = replay.currAtlas_.width;
var width = widths.t;
var widthX = widths.s + widths.o + widths.m + widths.e + widths.T +
widths.e + widths.x;
var charInfo = replay.currAtlas_.atlas.getInfo('t');
expect(replay.height).to.be(height);
expect(replay.width).to.be(width);
expect(replay.originX).to.be(charInfo.offsetX);
expect(replay.originY).to.be(charInfo.offsetY);
expect(replay.imageHeight).to.be(charInfo.image.height);
expect(replay.imageWidth).to.be(charInfo.image.width);
expect(replay.anchorX).to.be(-widthX + 10);
expect(replay.anchorY).to.be(10);
});
it('does not draw if text is empty', function() {
replay.state_.text = '';
replay.text_ = '';
var point;
point = [1000, 2000];
@@ -177,6 +169,126 @@ describe('ol.render.webgl.TextReplay', function() {
});
});
describe('#addCharToAtlas_', function() {
beforeEach(function() {
var textStyle = createTextStyle(
new ol.style.Fill({
color: [0, 0, 0, 1]
}),
null, 'someText');
replay.setTextStyle(textStyle);
});
it('adds a single character to the current atlas', function() {
var glyphAtlas = replay.currAtlas_.atlas;
var info;
replay.addCharToAtlas_('someText');
info = glyphAtlas.getInfo('someText');
expect(info).to.be(null);
replay.addCharToAtlas_('e');
replay.addCharToAtlas_('x');
info = glyphAtlas.getInfo('e');
expect(info).not.to.be(null);
info = glyphAtlas.getInfo('x');
expect(info).not.to.be(null);
});
it('keeps the atlas and the width dictionary synced', function() {
var glyphAtlas = replay.currAtlas_;
replay.addCharToAtlas_('e');
replay.addCharToAtlas_('x');
expect(Object.keys(glyphAtlas.width)).to.have.length(2);
replay.addCharToAtlas_('someText');
expect(Object.keys(glyphAtlas.width)).to.have.length(2);
});
});
describe('#getTextSize_', function() {
beforeEach(function() {
var textStyle = createTextStyle(
new ol.style.Fill({
color: [0, 0, 0, 1]
}),
null, 'someText');
textStyle.setScale(1);
replay.setTextStyle(textStyle);
});
it('adds missing characters to the current atlas', function() {
var glyphAtlas = replay.currAtlas_;
var info;
expect(Object.keys(glyphAtlas.width)).to.have.length(0);
replay.getTextSize_(['someText']);
expect(Object.keys(glyphAtlas.width)).to.have.length(7);
info = glyphAtlas.atlas.getInfo('s');
expect(info).not.to.be(null);
info = glyphAtlas.atlas.getInfo('o');
expect(info).not.to.be(null);
info = glyphAtlas.atlas.getInfo('m');
expect(info).not.to.be(null);
info = glyphAtlas.atlas.getInfo('e');
expect(info).not.to.be(null);
info = glyphAtlas.atlas.getInfo('T');
expect(info).not.to.be(null);
info = glyphAtlas.atlas.getInfo('x');
expect(info).not.to.be(null);
info = glyphAtlas.atlas.getInfo('t');
expect(info).not.to.be(null);
});
it('returns the size of the label\'s bounding box in pixels', function() {
var size;
var mCtx = ol.dom.createCanvasContext2D(0, 0);
mCtx.font = '12px Arial';
var width = mCtx.measureText('someText').width;
var width2 = mCtx.measureText('anEvenLongerLine').width;
var height = Math.ceil(mCtx.measureText('M').width * 1.5);
size = replay.getTextSize_(['someText']);
expect(size[0]).to.be.within(width, width + 8);
expect(size[1]).to.be(height);
size = replay.getTextSize_(['someText', 'anEvenLongerLine']);
expect(size[0]).to.be.within(width2, width2 + 16);
expect(size[1]).to.be(height * 2);
});
});
describe('#getAtlas_', function() {
beforeEach(function() {
var textStyle = createTextStyle(
new ol.style.Fill({
color: [0, 0, 0, 1]
}),
null, 'someText');
replay.setTextStyle(textStyle);
});
it('returns the appropriate atlas for the current state', function() {
var atlas = replay.currAtlas_;
var state = replay.state_;
expect(Object.keys(replay.atlases_)).to.have.length(1);
expect(replay.getAtlas_(state)).to.be(atlas);
expect(Object.keys(replay.atlases_)).to.have.length(1);
});
it('creates a new atlas if it cannot find the one for the current state', function() {
var atlas = replay.currAtlas_;
var state = replay.state_;
state.lineWidth = 50;
expect(Object.keys(replay.atlases_)).to.have.length(1);
expect(replay.getAtlas_(state)).not.to.be(atlas);
expect(Object.keys(replay.atlases_)).to.have.length(2);
});
});
describe('#getTextures', function() {
beforeEach(function() {
replay.textures_ = [1, 2];

View File

@@ -105,14 +105,14 @@ describe('ol.rendering.style.Text', function() {
it('tests the webgl renderer without rotation', function(done) {
map = createMap('webgl');
createFeatures();
expectResemble(map, 'spec/ol/style/expected/text-webgl.png', IMAGE_TOLERANCE, done);
expectResemble(map, 'spec/ol/style/expected/text-webgl.png', 1.8, done);
});
it('tests the webgl renderer with rotation', function(done) {
map = createMap('webgl');
createFeatures();
map.getView().setRotation(Math.PI / 7);
expectResemble(map, 'spec/ol/style/expected/text-rotated-webgl.png', 1.6, done);
expectResemble(map, 'spec/ol/style/expected/text-rotated-webgl.png', 1.8, done);
});
});