PinBuilder.js 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. import buildModuleUrl from "./buildModuleUrl.js";
  2. import Color from "./Color.js";
  3. import defined from "./defined.js";
  4. import DeveloperError from "./DeveloperError.js";
  5. import Resource from "./Resource.js";
  6. import writeTextToCanvas from "./writeTextToCanvas.js";
  7. /**
  8. * A utility class for generating custom map pins as canvas elements.
  9. * <br /><br />
  10. * <div align='center'>
  11. * <img src='Images/PinBuilder.png' width='500'/><br />
  12. * Example pins generated using both the maki icon set, which ships with Cesium, and single character text.
  13. * </div>
  14. *
  15. * @alias PinBuilder
  16. * @constructor
  17. *
  18. * @demo {@link https://sandcastle.cesium.com/index.html?src=Map%20Pins.html|Cesium Sandcastle PinBuilder Demo}
  19. */
  20. function PinBuilder() {
  21. this._cache = {};
  22. }
  23. /**
  24. * Creates an empty pin of the specified color and size.
  25. *
  26. * @param {Color} color The color of the pin.
  27. * @param {Number} size The size of the pin, in pixels.
  28. * @returns {HTMLCanvasElement} The canvas element that represents the generated pin.
  29. */
  30. PinBuilder.prototype.fromColor = function (color, size) {
  31. //>>includeStart('debug', pragmas.debug);
  32. if (!defined(color)) {
  33. throw new DeveloperError("color is required");
  34. }
  35. if (!defined(size)) {
  36. throw new DeveloperError("size is required");
  37. }
  38. //>>includeEnd('debug');
  39. return createPin(undefined, undefined, color, size, this._cache);
  40. };
  41. /**
  42. * Creates a pin with the specified icon, color, and size.
  43. *
  44. * @param {Resource|String} url The url of the image to be stamped onto the pin.
  45. * @param {Color} color The color of the pin.
  46. * @param {Number} size The size of the pin, in pixels.
  47. * @returns {HTMLCanvasElement|Promise.<HTMLCanvasElement>} The canvas element or a Promise to the canvas element that represents the generated pin.
  48. */
  49. PinBuilder.prototype.fromUrl = function (url, color, size) {
  50. //>>includeStart('debug', pragmas.debug);
  51. if (!defined(url)) {
  52. throw new DeveloperError("url is required");
  53. }
  54. if (!defined(color)) {
  55. throw new DeveloperError("color is required");
  56. }
  57. if (!defined(size)) {
  58. throw new DeveloperError("size is required");
  59. }
  60. //>>includeEnd('debug');
  61. return createPin(url, undefined, color, size, this._cache);
  62. };
  63. /**
  64. * Creates a pin with the specified {@link https://www.mapbox.com/maki/|maki} icon identifier, color, and size.
  65. *
  66. * @param {String} id The id of the maki icon to be stamped onto the pin.
  67. * @param {Color} color The color of the pin.
  68. * @param {Number} size The size of the pin, in pixels.
  69. * @returns {HTMLCanvasElement|Promise.<HTMLCanvasElement>} The canvas element or a Promise to the canvas element that represents the generated pin.
  70. */
  71. PinBuilder.prototype.fromMakiIconId = function (id, color, size) {
  72. //>>includeStart('debug', pragmas.debug);
  73. if (!defined(id)) {
  74. throw new DeveloperError("id is required");
  75. }
  76. if (!defined(color)) {
  77. throw new DeveloperError("color is required");
  78. }
  79. if (!defined(size)) {
  80. throw new DeveloperError("size is required");
  81. }
  82. //>>includeEnd('debug');
  83. return createPin(
  84. buildModuleUrl(`Assets/Textures/maki/${encodeURIComponent(id)}.png`),
  85. undefined,
  86. color,
  87. size,
  88. this._cache
  89. );
  90. };
  91. /**
  92. * Creates a pin with the specified text, color, and size. The text will be sized to be as large as possible
  93. * while still being contained completely within the pin.
  94. *
  95. * @param {String} text The text to be stamped onto the pin.
  96. * @param {Color} color The color of the pin.
  97. * @param {Number} size The size of the pin, in pixels.
  98. * @returns {HTMLCanvasElement} The canvas element that represents the generated pin.
  99. */
  100. PinBuilder.prototype.fromText = function (text, color, size) {
  101. //>>includeStart('debug', pragmas.debug);
  102. if (!defined(text)) {
  103. throw new DeveloperError("text is required");
  104. }
  105. if (!defined(color)) {
  106. throw new DeveloperError("color is required");
  107. }
  108. if (!defined(size)) {
  109. throw new DeveloperError("size is required");
  110. }
  111. //>>includeEnd('debug');
  112. return createPin(undefined, text, color, size, this._cache);
  113. };
  114. const colorScratch = new Color();
  115. //This function (except for the 3 commented lines) was auto-generated from an online tool,
  116. //http://www.professorcloud.com/svg-to-canvas/, using Assets/Textures/pin.svg as input.
  117. //The reason we simply can't load and draw the SVG directly to the canvas is because
  118. //it taints the canvas in Internet Explorer (and possibly some other browsers); making
  119. //it impossible to create a WebGL texture from the result.
  120. function drawPin(context2D, color, size) {
  121. context2D.save();
  122. context2D.scale(size / 24, size / 24); //Added to auto-generated code to scale up to desired size.
  123. context2D.fillStyle = color.toCssColorString(); //Modified from auto-generated code.
  124. context2D.strokeStyle = color.brighten(0.6, colorScratch).toCssColorString(); //Modified from auto-generated code.
  125. context2D.lineWidth = 0.846;
  126. context2D.beginPath();
  127. context2D.moveTo(6.72, 0.422);
  128. context2D.lineTo(17.28, 0.422);
  129. context2D.bezierCurveTo(18.553, 0.422, 19.577, 1.758, 19.577, 3.415);
  130. context2D.lineTo(19.577, 10.973);
  131. context2D.bezierCurveTo(19.577, 12.63, 18.553, 13.966, 17.282, 13.966);
  132. context2D.lineTo(14.386, 14.008);
  133. context2D.lineTo(11.826, 23.578);
  134. context2D.lineTo(9.614, 14.008);
  135. context2D.lineTo(6.719, 13.965);
  136. context2D.bezierCurveTo(5.446, 13.983, 4.422, 12.629, 4.422, 10.972);
  137. context2D.lineTo(4.422, 3.416);
  138. context2D.bezierCurveTo(4.423, 1.76, 5.447, 0.423, 6.718, 0.423);
  139. context2D.closePath();
  140. context2D.fill();
  141. context2D.stroke();
  142. context2D.restore();
  143. }
  144. //This function takes an image or canvas and uses it as a template
  145. //to "stamp" the pin with a white image outlined in black. The color
  146. //values of the input image are ignored completely and only the alpha
  147. //values are used.
  148. function drawIcon(context2D, image, size) {
  149. //Size is the largest image that looks good inside of pin box.
  150. const imageSize = size / 2.5;
  151. let sizeX = imageSize;
  152. let sizeY = imageSize;
  153. if (image.width > image.height) {
  154. sizeY = imageSize * (image.height / image.width);
  155. } else if (image.width < image.height) {
  156. sizeX = imageSize * (image.width / image.height);
  157. }
  158. //x and y are the center of the pin box
  159. const x = Math.round((size - sizeX) / 2);
  160. const y = Math.round((7 / 24) * size - sizeY / 2);
  161. context2D.globalCompositeOperation = "destination-out";
  162. context2D.drawImage(image, x - 1, y, sizeX, sizeY);
  163. context2D.drawImage(image, x, y - 1, sizeX, sizeY);
  164. context2D.drawImage(image, x + 1, y, sizeX, sizeY);
  165. context2D.drawImage(image, x, y + 1, sizeX, sizeY);
  166. context2D.globalCompositeOperation = "destination-over";
  167. context2D.fillStyle = Color.BLACK.toCssColorString();
  168. context2D.fillRect(x - 1, y - 1, sizeX + 2, sizeY + 2);
  169. context2D.globalCompositeOperation = "destination-out";
  170. context2D.drawImage(image, x, y, sizeX, sizeY);
  171. context2D.globalCompositeOperation = "destination-over";
  172. context2D.fillStyle = Color.WHITE.toCssColorString();
  173. context2D.fillRect(x - 1, y - 2, sizeX + 2, sizeY + 2);
  174. }
  175. const stringifyScratch = new Array(4);
  176. function createPin(url, label, color, size, cache) {
  177. //Use the parameters as a unique ID for caching.
  178. stringifyScratch[0] = url;
  179. stringifyScratch[1] = label;
  180. stringifyScratch[2] = color;
  181. stringifyScratch[3] = size;
  182. const id = JSON.stringify(stringifyScratch);
  183. const item = cache[id];
  184. if (defined(item)) {
  185. return item;
  186. }
  187. const canvas = document.createElement("canvas");
  188. canvas.width = size;
  189. canvas.height = size;
  190. const context2D = canvas.getContext("2d");
  191. drawPin(context2D, color, size);
  192. if (defined(url)) {
  193. const resource = Resource.createIfNeeded(url);
  194. //If we have an image url, load it and then stamp the pin.
  195. const promise = resource.fetchImage().then(function (image) {
  196. drawIcon(context2D, image, size);
  197. cache[id] = canvas;
  198. return canvas;
  199. });
  200. cache[id] = promise;
  201. return promise;
  202. } else if (defined(label)) {
  203. //If we have a label, write it to a canvas and then stamp the pin.
  204. const image = writeTextToCanvas(label, {
  205. font: `bold ${size}px sans-serif`,
  206. });
  207. drawIcon(context2D, image, size);
  208. }
  209. cache[id] = canvas;
  210. return canvas;
  211. }
  212. export default PinBuilder;