PinBuilder.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  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 {Canvas} 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 {Canvas|Promise.<Canvas>} 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 {Canvas|Promise.<Canvas>} 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(buildModuleUrl('Assets/Textures/maki/' + encodeURIComponent(id) + '.png'), undefined, color, size, this._cache);
  84. };
  85. /**
  86. * Creates a pin with the specified text, color, and size. The text will be sized to be as large as possible
  87. * while still being contained completely within the pin.
  88. *
  89. * @param {String} text The text to be stamped onto the pin.
  90. * @param {Color} color The color of the pin.
  91. * @param {Number} size The size of the pin, in pixels.
  92. * @returns {Canvas} The canvas element that represents the generated pin.
  93. */
  94. PinBuilder.prototype.fromText = function(text, color, size) {
  95. //>>includeStart('debug', pragmas.debug);
  96. if (!defined(text)) {
  97. throw new DeveloperError('text is required');
  98. }
  99. if (!defined(color)) {
  100. throw new DeveloperError('color is required');
  101. }
  102. if (!defined(size)) {
  103. throw new DeveloperError('size is required');
  104. }
  105. //>>includeEnd('debug');
  106. return createPin(undefined, text, color, size, this._cache);
  107. };
  108. var colorScratch = new Color();
  109. //This function (except for the 3 commented lines) was auto-generated from an online tool,
  110. //http://www.professorcloud.com/svg-to-canvas/, using Assets/Textures/pin.svg as input.
  111. //The reason we simply can't load and draw the SVG directly to the canvas is because
  112. //it taints the canvas in Internet Explorer (and possibly some other browsers); making
  113. //it impossible to create a WebGL texture from the result.
  114. function drawPin(context2D, color, size) {
  115. context2D.save();
  116. context2D.scale(size / 24, size / 24); //Added to auto-generated code to scale up to desired size.
  117. context2D.fillStyle = color.toCssColorString(); //Modified from auto-generated code.
  118. context2D.strokeStyle = color.brighten(0.6, colorScratch).toCssColorString(); //Modified from auto-generated code.
  119. context2D.lineWidth = 0.846;
  120. context2D.beginPath();
  121. context2D.moveTo(6.72, 0.422);
  122. context2D.lineTo(17.28, 0.422);
  123. context2D.bezierCurveTo(18.553, 0.422, 19.577, 1.758, 19.577, 3.415);
  124. context2D.lineTo(19.577, 10.973);
  125. context2D.bezierCurveTo(19.577, 12.63, 18.553, 13.966, 17.282, 13.966);
  126. context2D.lineTo(14.386, 14.008);
  127. context2D.lineTo(11.826, 23.578);
  128. context2D.lineTo(9.614, 14.008);
  129. context2D.lineTo(6.719, 13.965);
  130. context2D.bezierCurveTo(5.446, 13.983, 4.422, 12.629, 4.422, 10.972);
  131. context2D.lineTo(4.422, 3.416);
  132. context2D.bezierCurveTo(4.423, 1.76, 5.447, 0.423, 6.718, 0.423);
  133. context2D.closePath();
  134. context2D.fill();
  135. context2D.stroke();
  136. context2D.restore();
  137. }
  138. //This function takes an image or canvas and uses it as a template
  139. //to "stamp" the pin with a white image outlined in black. The color
  140. //values of the input image are ignored completely and only the alpha
  141. //values are used.
  142. function drawIcon(context2D, image, size) {
  143. //Size is the largest image that looks good inside of pin box.
  144. var imageSize = size / 2.5;
  145. var sizeX = imageSize;
  146. var sizeY = imageSize;
  147. if (image.width > image.height) {
  148. sizeY = imageSize * (image.height / image.width);
  149. } else if (image.width < image.height) {
  150. sizeX = imageSize * (image.width / image.height);
  151. }
  152. //x and y are the center of the pin box
  153. var x = Math.round((size - sizeX) / 2);
  154. var y = Math.round(((7 / 24) * size) - (sizeY / 2));
  155. context2D.globalCompositeOperation = 'destination-out';
  156. context2D.drawImage(image, x - 1, y, sizeX, sizeY);
  157. context2D.drawImage(image, x, y - 1, sizeX, sizeY);
  158. context2D.drawImage(image, x + 1, y, sizeX, sizeY);
  159. context2D.drawImage(image, x, y + 1, sizeX, sizeY);
  160. context2D.globalCompositeOperation = 'destination-over';
  161. context2D.fillStyle = Color.BLACK.toCssColorString();
  162. context2D.fillRect(x - 1, y - 1, sizeX + 2, sizeY + 2);
  163. context2D.globalCompositeOperation = 'destination-out';
  164. context2D.drawImage(image, x, y, sizeX, sizeY);
  165. context2D.globalCompositeOperation = 'destination-over';
  166. context2D.fillStyle = Color.WHITE.toCssColorString();
  167. context2D.fillRect(x - 1, y - 2, sizeX + 2, sizeY + 2);
  168. }
  169. var stringifyScratch = new Array(4);
  170. function createPin(url, label, color, size, cache) {
  171. //Use the parameters as a unique ID for caching.
  172. stringifyScratch[0] = url;
  173. stringifyScratch[1] = label;
  174. stringifyScratch[2] = color;
  175. stringifyScratch[3] = size;
  176. var id = JSON.stringify(stringifyScratch);
  177. var item = cache[id];
  178. if (defined(item)) {
  179. return item;
  180. }
  181. var canvas = document.createElement('canvas');
  182. canvas.width = size;
  183. canvas.height = size;
  184. var context2D = canvas.getContext('2d');
  185. drawPin(context2D, color, size);
  186. if (defined(url)) {
  187. var resource = Resource.createIfNeeded(url);
  188. //If we have an image url, load it and then stamp the pin.
  189. var promise = resource.fetchImage().then(function(image) {
  190. drawIcon(context2D, image, size);
  191. cache[id] = canvas;
  192. return canvas;
  193. });
  194. cache[id] = promise;
  195. return promise;
  196. } else if (defined(label)) {
  197. //If we have a label, write it to a canvas and then stamp the pin.
  198. var image = writeTextToCanvas(label, {
  199. font : 'bold ' + size + 'px sans-serif'
  200. });
  201. drawIcon(context2D, image, size);
  202. }
  203. cache[id] = canvas;
  204. return canvas;
  205. }
  206. export default PinBuilder;