Commit defe6516 authored by David Schnur's avatar David Schnur

Allow the same text in multiple locations.

Resolves #1032.  Previously it was impossible to draw the same text,
with the same style, in two different locations, because the second
would end up using the first's cache entry, which only ended up moving
the element to a new position.

Now each cache entry holds a list of positions at which the text
appears, creating clones of the original element for each position
beyond the first.
parent 77a4b864
...@@ -79,12 +79,9 @@ browser, but needs to redraw with canvas text when exporting as an image. ...@@ -79,12 +79,9 @@ browser, but needs to redraw with canvas text when exporting as an image.
for (var key in styleCache) { for (var key in styleCache) {
if (hasOwnProperty.call(styleCache, key)) { if (hasOwnProperty.call(styleCache, key)) {
var info = styleCache[key]; var info = styleCache[key],
positions = info.positions,
if (!info.active) { lines = info.lines;
delete styleCache[key];
continue;
}
// Since every element at this level of the cache have the // Since every element at this level of the cache have the
// same font and fill styles, we can just change them once // same font and fill styles, we can just change them once
...@@ -96,10 +93,18 @@ browser, but needs to redraw with canvas text when exporting as an image. ...@@ -96,10 +93,18 @@ browser, but needs to redraw with canvas text when exporting as an image.
updateStyles = false; updateStyles = false;
} }
var lines = info.lines; for (var i = 0, position; position = positions[i]; i++) {
for (var i = 0; i < lines.length; ++i) { if (position.active) {
var line = lines[i]; for (var j = 0, line; line = position.lines[j]; j++) {
context.fillText(line.text, line.x, line.y); context.fillText(lines[j].text, line[0], line[1]);
}
} else {
positions.splice(i--, 1);
}
}
if (positions.length == 0) {
delete styleCache[key];
} }
} }
} }
...@@ -116,11 +121,9 @@ browser, but needs to redraw with canvas text when exporting as an image. ...@@ -116,11 +121,9 @@ browser, but needs to redraw with canvas text when exporting as an image.
// When the canvas option is set, the object looks like this: // When the canvas option is set, the object looks like this:
// //
// { // {
// x: X coordinate at which the text is located.
// x: Y coordinate at which the text is located.
// width: Width of the text's bounding box. // width: Width of the text's bounding box.
// height: Height of the text's bounding box. // height: Height of the text's bounding box.
// active: Flag indicating whether the text should be visible. // positions: Array of positions at which this text is drawn.
// lines: [{ // lines: [{
// height: Height of this line. // height: Height of this line.
// widths: Width of this line. // widths: Width of this line.
...@@ -131,6 +134,15 @@ browser, but needs to redraw with canvas text when exporting as an image. ...@@ -131,6 +134,15 @@ browser, but needs to redraw with canvas text when exporting as an image.
// color: Color of the text. // color: Color of the text.
// }, // },
// } // }
//
// The positions array contains objects that look like this:
//
// {
// active: Flag indicating whether the text should be visible.
// lines: Array of [x, y] coordinates at which to draw the line.
// x: X coordinate at which to draw the text.
// y: Y coordinate at which to draw the text.
// }
Canvas.prototype.getTextInfo = function(layer, text, font, angle, width) { Canvas.prototype.getTextInfo = function(layer, text, font, angle, width) {
...@@ -210,7 +222,7 @@ browser, but needs to redraw with canvas text when exporting as an image. ...@@ -210,7 +222,7 @@ browser, but needs to redraw with canvas text when exporting as an image.
info = styleCache[text] = { info = styleCache[text] = {
width: 0, width: 0,
height: 0, height: 0,
active: false, positions: [],
lines: [], lines: [],
font: { font: {
definition: textStyle, definition: textStyle,
...@@ -258,12 +270,9 @@ browser, but needs to redraw with canvas text when exporting as an image. ...@@ -258,12 +270,9 @@ browser, but needs to redraw with canvas text when exporting as an image.
} }
var info = this.getTextInfo(layer, text, font, angle, width), var info = this.getTextInfo(layer, text, font, angle, width),
positions = info.positions,
lines = info.lines; lines = info.lines;
// Mark the text for inclusion in the next render pass
info.active = true;
// Text is drawn with baseline 'middle', which we need to account // Text is drawn with baseline 'middle', which we need to account
// for by adding half a line's height to the y position. // for by adding half a line's height to the y position.
...@@ -289,20 +298,39 @@ browser, but needs to redraw with canvas text when exporting as an image. ...@@ -289,20 +298,39 @@ browser, but needs to redraw with canvas text when exporting as an image.
y -= 2; y -= 2;
} }
// Determine whether this text already exists at this position.
// If so, mark it for inclusion in the next render pass.
for (var i = 0, position; position = positions[i]; i++) {
if (position.x == x && position.y == y) {
position.active = true;
return;
}
}
// If the text doesn't exist at this position, create a new entry
position = {
active: true,
lines: [],
x: x,
y: y
};
positions.push(position);
// Fill in the x & y positions of each line, adjusting them // Fill in the x & y positions of each line, adjusting them
// individually for horizontal alignment. // individually for horizontal alignment.
for (var i = 0; i < lines.length; ++i) { for (var i = 0, line; line = lines[i]; i++) {
var line = lines[i];
line.y = y;
y += line.height;
if (halign == "center") { if (halign == "center") {
line.x = Math.round(x - line.width / 2); position.lines.push([Math.round(x - line.width / 2), y]);
} else if (halign == "right") { } else if (halign == "right") {
line.x = Math.round(x - line.width); position.lines.push([Math.round(x - line.width), y]);
} else { } else {
line.x = Math.round(x); position.lines.push([Math.round(x), y]);
} }
y += line.height;
} }
}; };
} }
......
...@@ -184,19 +184,27 @@ Licensed under the MIT license. ...@@ -184,19 +184,27 @@ Licensed under the MIT license.
var styleCache = layerCache[styleKey]; var styleCache = layerCache[styleKey];
for (var key in styleCache) { for (var key in styleCache) {
if (hasOwnProperty.call(styleCache, key)) { if (hasOwnProperty.call(styleCache, key)) {
var info = styleCache[key];
if (info.active) { var positions = styleCache[key].positions;
if (!info.rendered) {
layer.append(info.element); for (var i = 0, position; position = positions[i]; i++) {
info.rendered = true; if (position.active) {
if (!position.rendered) {
layer.append(position.element);
position.rendered = true;
} }
} else { } else {
delete styleCache[key]; positions.splice(i--, 1);
if (info.rendered) { if (position.rendered) {
info.element.detach(); position.element.detach();
} }
} }
} }
if (positions.length == 0) {
delete styleCache[key];
}
}
} }
} }
} }
...@@ -258,11 +266,26 @@ Licensed under the MIT license. ...@@ -258,11 +266,26 @@ Licensed under the MIT license.
// { // {
// width: Width of the text's wrapper div. // width: Width of the text's wrapper div.
// height: Height of the text's wrapper div. // height: Height of the text's wrapper div.
// element: The jQuery-wrapped HTML div containing the text.
// positions: Array of positions at which this text is drawn.
// }
//
// The positions array contains objects that look like this:
//
// {
// active: Flag indicating whether the text should be visible. // active: Flag indicating whether the text should be visible.
// rendered: Flag indicating whether the text is currently visible. // rendered: Flag indicating whether the text is currently visible.
// element: The jQuery-wrapped HTML div containing the text. // element: The jQuery-wrapped HTML div containing the text.
// x: X coordinate at which to draw the text.
// y: Y coordinate at which to draw the text.
// } // }
// //
// Each position after the first receives a clone of the original element.
//
// The idea is that that the width, height, and general 'identity' of the
// text is constant no matter where it is placed; the placements are a
// secondary property.
//
// Canvas maintains a cache of recently-used text info objects; getTextInfo // Canvas maintains a cache of recently-used text info objects; getTextInfo
// either returns the cached element or creates a new entry. // either returns the cached element or creates a new entry.
// //
...@@ -330,11 +353,10 @@ Licensed under the MIT license. ...@@ -330,11 +353,10 @@ Licensed under the MIT license.
} }
info = styleCache[text] = { info = styleCache[text] = {
active: false,
rendered: false,
element: element,
width: element.outerWidth(true), width: element.outerWidth(true),
height: element.outerHeight(true) height: element.outerHeight(true),
element: element,
positions: []
}; };
element.detach(); element.detach();
...@@ -365,11 +387,8 @@ Licensed under the MIT license. ...@@ -365,11 +387,8 @@ Licensed under the MIT license.
Canvas.prototype.addText = function(layer, x, y, text, font, angle, width, halign, valign) { Canvas.prototype.addText = function(layer, x, y, text, font, angle, width, halign, valign) {
var info = this.getTextInfo(layer, text, font, angle, width); var info = this.getTextInfo(layer, text, font, angle, width),
positions = info.positions;
// Mark the div for inclusion in the next render pass
info.active = true;
// Tweak the div's position to match the text's alignment // Tweak the div's position to match the text's alignment
...@@ -385,9 +404,34 @@ Licensed under the MIT license. ...@@ -385,9 +404,34 @@ Licensed under the MIT license.
y -= info.height; y -= info.height;
} }
// Determine whether this text already exists at this position.
// If so, mark it for inclusion in the next render pass.
for (var i = 0, position; position = positions[i]; i++) {
if (position.x == x && position.y == y) {
position.active = true;
return;
}
}
// If the text doesn't exist at this position, create a new entry
// For the very first position we'll re-use the original element,
// while for subsequent ones we'll clone it.
position = {
active: true,
rendered: false,
element: positions.length ? info.element.clone() : info.element,
x: x,
y: y
}
positions.push(position);
// Move the element to its final position within the container // Move the element to its final position within the container
info.element.css({ position.element.css({
top: Math.round(y), top: Math.round(y),
left: Math.round(x), left: Math.round(x),
'text-align': halign // In case the text wraps 'text-align': halign // In case the text wraps
...@@ -397,18 +441,24 @@ Licensed under the MIT license. ...@@ -397,18 +441,24 @@ Licensed under the MIT license.
// Removes one or more text strings from the canvas text overlay. // Removes one or more text strings from the canvas text overlay.
// //
// If no parameters are given, all text within the layer is removed. // If no parameters are given, all text within the layer is removed.
// The text is not actually removed; it is simply marked as inactive, which //
// will result in its removal on the next render pass. // Note that the text is not immediately removed; it is simply marked as
// inactive, which will result in its removal on the next render pass.
// This avoids the performance penalty for 'clear and redraw' behavior,
// where we potentially get rid of all text on a layer, but will likely
// add back most or all of it later, as when redrawing axes, for example.
// //
// @param {string} layer A string of space-separated CSS classes uniquely // @param {string} layer A string of space-separated CSS classes uniquely
// identifying the layer containing this text. // identifying the layer containing this text.
// @param {string} text Text string to remove. // @param {number=} x X coordinate of the text.
// @param {number=} y Y coordinate of the text.
// @param {string=} text Text string to remove.
// @param {(string|object)=} font Either a string of space-separated CSS // @param {(string|object)=} font Either a string of space-separated CSS
// classes or a font-spec object, defining the text's font and style. // classes or a font-spec object, defining the text's font and style.
// @param {number=} angle Angle at which the text is rotated, in degrees. // @param {number=} angle Angle at which the text is rotated, in degrees.
// Angle is currently unused, it will be implemented in the future. // Angle is currently unused, it will be implemented in the future.
Canvas.prototype.removeText = function(layer, text, font, angle) { Canvas.prototype.removeText = function(layer, x, y, text, font, angle) {
if (text == null) { if (text == null) {
var layerCache = this._textCache[layer]; var layerCache = this._textCache[layer];
if (layerCache != null) { if (layerCache != null) {
...@@ -417,14 +467,22 @@ Licensed under the MIT license. ...@@ -417,14 +467,22 @@ Licensed under the MIT license.
var styleCache = layerCache[styleKey]; var styleCache = layerCache[styleKey];
for (var key in styleCache) { for (var key in styleCache) {
if (hasOwnProperty.call(styleCache, key)) { if (hasOwnProperty.call(styleCache, key)) {
styleCache[key].active = false; var positions = styleCache[key].positions;
for (var i = 0, position; position = positions[i]; i++) {
position.active = false;
}
} }
} }
} }
} }
} }
} else { } else {
this.getTextInfo(layer, text, font, angle).active = false; var positions = this.getTextInfo(layer, text, font, angle).positions;
for (var i = 0, position; position = positions[i]; i++) {
if (position.x == x && position.y == y) {
position.active = false;
}
}
} }
}; };
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment