writeTextToCanvas.js 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. import Color from "./Color.js";
  2. import defaultValue from "./defaultValue.js";
  3. import defined from "./defined.js";
  4. import DeveloperError from "./DeveloperError.js";
  5. function measureText(context2D, textString, font, stroke, fill) {
  6. const metrics = context2D.measureText(textString);
  7. const isSpace = !/\S/.test(textString);
  8. if (!isSpace) {
  9. const fontSize = document.defaultView
  10. .getComputedStyle(context2D.canvas)
  11. .getPropertyValue("font-size")
  12. .replace("px", "");
  13. const canvas = document.createElement("canvas");
  14. const padding = 100;
  15. const width = (metrics.width + padding) | 0;
  16. const height = 3 * fontSize;
  17. const baseline = height / 2;
  18. canvas.width = width;
  19. canvas.height = height;
  20. const ctx = canvas.getContext("2d");
  21. ctx.font = font;
  22. ctx.fillStyle = "white";
  23. ctx.fillRect(0, 0, canvas.width + 1, canvas.height + 1);
  24. if (stroke) {
  25. ctx.strokeStyle = "black";
  26. ctx.lineWidth = context2D.lineWidth;
  27. ctx.strokeText(textString, padding / 2, baseline);
  28. }
  29. if (fill) {
  30. ctx.fillStyle = "black";
  31. ctx.fillText(textString, padding / 2, baseline);
  32. }
  33. // Context image data has width * height * 4 elements, because
  34. // each pixel's R, G, B and A are consecutive values in the array.
  35. const pixelData = ctx.getImageData(0, 0, width, height).data;
  36. const length = pixelData.length;
  37. const width4 = width * 4;
  38. let i, j;
  39. let ascent, descent;
  40. // Find the number of rows (from the top) until the first non-white pixel
  41. for (i = 0; i < length; ++i) {
  42. if (pixelData[i] !== 255) {
  43. ascent = (i / width4) | 0;
  44. break;
  45. }
  46. }
  47. // Find the number of rows (from the bottom) until the first non-white pixel
  48. for (i = length - 1; i >= 0; --i) {
  49. if (pixelData[i] !== 255) {
  50. descent = (i / width4) | 0;
  51. break;
  52. }
  53. }
  54. let minx = -1;
  55. // For each column, for each row, check for first non-white pixel
  56. for (i = 0; i < width && minx === -1; ++i) {
  57. for (j = 0; j < height; ++j) {
  58. const pixelIndex = i * 4 + j * width4;
  59. if (
  60. pixelData[pixelIndex] !== 255 ||
  61. pixelData[pixelIndex + 1] !== 255 ||
  62. pixelData[pixelIndex + 2] !== 255 ||
  63. pixelData[pixelIndex + 3] !== 255
  64. ) {
  65. minx = i;
  66. break;
  67. }
  68. }
  69. }
  70. return {
  71. width: metrics.width,
  72. height: descent - ascent,
  73. ascent: baseline - ascent,
  74. descent: descent - baseline,
  75. minx: minx - padding / 2,
  76. };
  77. }
  78. return {
  79. width: metrics.width,
  80. height: 0,
  81. ascent: 0,
  82. descent: 0,
  83. minx: 0,
  84. };
  85. }
  86. let imageSmoothingEnabledName;
  87. /**
  88. * Writes the given text into a new canvas. The canvas will be sized to fit the text.
  89. * If text is blank, returns undefined.
  90. *
  91. * @param {string} text The text to write.
  92. * @param {object} [options] Object with the following properties:
  93. * @param {string} [options.font='10px sans-serif'] The CSS font to use.
  94. * @param {string} [options.textBaseline='bottom'] The baseline of the text.
  95. * @param {boolean} [options.fill=true] Whether to fill the text.
  96. * @param {boolean} [options.stroke=false] Whether to stroke the text.
  97. * @param {Color} [options.fillColor=Color.WHITE] The fill color.
  98. * @param {Color} [options.strokeColor=Color.BLACK] The stroke color.
  99. * @param {number} [options.strokeWidth=1] The stroke width.
  100. * @param {Color} [options.backgroundColor=Color.TRANSPARENT] The background color of the canvas.
  101. * @param {number} [options.padding=0] The pixel size of the padding to add around the text.
  102. * @returns {HTMLCanvasElement|undefined} A new canvas with the given text drawn into it. The dimensions object
  103. * from measureText will also be added to the returned canvas. If text is
  104. * blank, returns undefined.
  105. * @function writeTextToCanvas
  106. */
  107. function writeTextToCanvas(text, options) {
  108. //>>includeStart('debug', pragmas.debug);
  109. if (!defined(text)) {
  110. throw new DeveloperError("text is required.");
  111. }
  112. //>>includeEnd('debug');
  113. if (text === "") {
  114. return undefined;
  115. }
  116. options = defaultValue(options, defaultValue.EMPTY_OBJECT);
  117. const font = defaultValue(options.font, "10px sans-serif");
  118. const stroke = defaultValue(options.stroke, false);
  119. const fill = defaultValue(options.fill, true);
  120. const strokeWidth = defaultValue(options.strokeWidth, 1);
  121. const backgroundColor = defaultValue(
  122. options.backgroundColor,
  123. Color.TRANSPARENT
  124. );
  125. const padding = defaultValue(options.padding, 0);
  126. const doublePadding = padding * 2.0;
  127. const canvas = document.createElement("canvas");
  128. canvas.width = 1;
  129. canvas.height = 1;
  130. canvas.style.font = font;
  131. // Since multiple read-back operations are expected for labels, use the willReadFrequently option – See https://html.spec.whatwg.org/multipage/canvas.html#concept-canvas-will-read-frequently
  132. const context2D = canvas.getContext("2d", { willReadFrequently: true });
  133. if (!defined(imageSmoothingEnabledName)) {
  134. if (defined(context2D.imageSmoothingEnabled)) {
  135. imageSmoothingEnabledName = "imageSmoothingEnabled";
  136. } else if (defined(context2D.mozImageSmoothingEnabled)) {
  137. imageSmoothingEnabledName = "mozImageSmoothingEnabled";
  138. } else if (defined(context2D.webkitImageSmoothingEnabled)) {
  139. imageSmoothingEnabledName = "webkitImageSmoothingEnabled";
  140. } else if (defined(context2D.msImageSmoothingEnabled)) {
  141. imageSmoothingEnabledName = "msImageSmoothingEnabled";
  142. }
  143. }
  144. context2D.font = font;
  145. context2D.lineJoin = "round";
  146. context2D.lineWidth = strokeWidth;
  147. context2D[imageSmoothingEnabledName] = false;
  148. // in order for measureText to calculate style, the canvas has to be
  149. // (temporarily) added to the DOM.
  150. canvas.style.visibility = "hidden";
  151. document.body.appendChild(canvas);
  152. const dimensions = measureText(context2D, text, font, stroke, fill);
  153. // Set canvas.dimensions to be accessed in LabelCollection
  154. canvas.dimensions = dimensions;
  155. document.body.removeChild(canvas);
  156. canvas.style.visibility = "";
  157. // Some characters, such as the letter j, have a non-zero starting position.
  158. // This value is used for kerning later, but we need to take it into account
  159. // now in order to draw the text completely on the canvas
  160. const x = -dimensions.minx;
  161. // Expand the width to include the starting position.
  162. const width = Math.ceil(dimensions.width) + x + doublePadding;
  163. // While the height of the letter is correct, we need to adjust
  164. // where we start drawing it so that letters like j and y properly dip
  165. // below the line.
  166. const height = dimensions.height + doublePadding;
  167. const baseline = height - dimensions.ascent + padding;
  168. const y = height - baseline + doublePadding;
  169. canvas.width = width;
  170. canvas.height = height;
  171. // Properties must be explicitly set again after changing width and height
  172. context2D.font = font;
  173. context2D.lineJoin = "round";
  174. context2D.lineWidth = strokeWidth;
  175. context2D[imageSmoothingEnabledName] = false;
  176. // Draw background
  177. if (backgroundColor !== Color.TRANSPARENT) {
  178. context2D.fillStyle = backgroundColor.toCssColorString();
  179. context2D.fillRect(0, 0, canvas.width, canvas.height);
  180. }
  181. if (stroke) {
  182. const strokeColor = defaultValue(options.strokeColor, Color.BLACK);
  183. context2D.strokeStyle = strokeColor.toCssColorString();
  184. context2D.strokeText(text, x + padding, y);
  185. }
  186. if (fill) {
  187. const fillColor = defaultValue(options.fillColor, Color.WHITE);
  188. context2D.fillStyle = fillColor.toCssColorString();
  189. context2D.fillText(text, x + padding, y);
  190. }
  191. return canvas;
  192. }
  193. export default writeTextToCanvas;