jsketch.svg.js

  1. /*!
  2. * jSketch SVG 1.0 | Luis A. Leiva | MIT license
  3. */
  4. // XXX: Requires `jsketch.js` to be loaded first.
  5. /**
  6. * SVG serializer for the jSketch lib.
  7. * @version 1.0
  8. * @author Luis A. Leiva
  9. * @license MIT license
  10. */
  11. ;(function(window) {
  12. /**
  13. * Convert jSketch canvas to SVG.
  14. * This method is asynchronous, and will be invoked when the SVG is ready.
  15. * @param {function} callback - Callback function, to be invoked with the SVG (string) as argument.
  16. * @return {jSketch}
  17. * @memberof jSketch
  18. * @method
  19. * @name toSVG
  20. */
  21. window.jSketch.prototype.toSVG = function(callback) {
  22. // No callback, no SVG.
  23. if (typeof callback !== 'function') throw new Error('You must supply a callback in toSVG() method.');
  24. // Save pointer for use in closures.
  25. var self = this;
  26. // Gather lines together until path is closed.
  27. var paths = [];
  28. // TODO: Save composite operations. See https://www.w3.org/TR/2002/WD-SVG11-20020215/masking.html
  29. var comps = {};
  30. // Flag async stuff, like image loading.
  31. var asyncOps = 0;
  32. // Process jSketch properties.
  33. var graphics = {};
  34. // Process jSketch methods.
  35. var methods = {
  36. /**
  37. * Add async flag.
  38. * @return {string}
  39. */
  40. addAsync: function() {
  41. asyncOps++;
  42. return '';
  43. },
  44. /**
  45. * Remove async flag.
  46. * @return {string}
  47. */
  48. removeAsync: function() {
  49. asyncOps--;
  50. return '';
  51. },
  52. /**
  53. * Serialize image.
  54. * @param {object} img - Image element.
  55. * @param {number} [x] - Horizontal coordinate.
  56. * @param {number} [y] - Vertical coordinate.
  57. * @return {string}
  58. */
  59. drawImage: function(img, x, y) {
  60. return '<image preserveAspectRatio="none" \
  61. width="' + img.width + '" \
  62. height="' + img.height + '" \
  63. transform="translate('+ x +', '+ y +')" \
  64. xlink:href="' + img.src + '"></image>';
  65. },
  66. /**
  67. * Serialize filled rectangle.
  68. * @param {number} x - Horizontal coordinate.
  69. * @param {number} y - Vertical coordinate.
  70. * @param {number} width - Rectangle width.
  71. * @param {number} height - Rectangle height.
  72. * @return {string}
  73. */
  74. fillRect: function(x, y, width, height) {
  75. return '<rect stroke="none" \
  76. width="' + width + '" \
  77. height="' + height + '" \
  78. x="' + x + '" \
  79. y="' + y + '" \
  80. fill="' + graphics.fillStyle + '"></rect>';
  81. },
  82. /**
  83. * Serialize stroked rectangle.
  84. * @param {number} x - Horizontal coordinate.
  85. * @param {number} y - Vertical coordinate.
  86. * @param {number} width - Rectangle width.
  87. * @param {number} height - Rectangle height.
  88. * @return {string}
  89. */
  90. strokeRect: function(x, y, width, height) {
  91. return '<rect fill="none" \
  92. width="' + width + '" \
  93. height="' + height + '" \
  94. x="' + x + '" \
  95. y="' + y + '" \
  96. stroke="' + graphics.strokeStyle + '" \
  97. stroke-width="' + graphics.lineWidth + '" \
  98. stroke-linecap="' + graphics.lineCap + '" \
  99. stroke-linejoin="' + graphics.lineJoin + '" \
  100. stroke-miterlimit="' + graphics.miterLimit + '"></rect>';
  101. },
  102. /**
  103. * Serialize stroked circle.
  104. * @param {number} x - Horizontal coordinate.
  105. * @param {number} y - Vertical coordinate.
  106. * @param {number} radius - Circle radius.
  107. * @return {string}
  108. */
  109. strokeCircle: function(x, y, radius) {
  110. return '<circle fill="none" \
  111. cx="' + x + '" \
  112. cy="' + y + '" \
  113. r="' + radius + '" \
  114. stroke="' + graphics.strokeStyle + '" \
  115. stroke-width="' + graphics.lineWidth + '" />';
  116. },
  117. /**
  118. * Serialize filled circle.
  119. * @param {number} x - Horizontal coordinate.
  120. * @param {number} y - Vertical coordinate.
  121. * @param {number} radius - Circle radius.
  122. * @return {string}
  123. */
  124. fillCircle: function(x, y, radius) {
  125. return '<circle stroke="none" \
  126. cx="' + x + '" \
  127. cy="' + y + '" \
  128. r="' + radius + '" \
  129. fill="' + graphics.fillStyle + '" />';
  130. },
  131. /**
  132. * Mark start of path.
  133. * @return {string}
  134. */
  135. beginPath: function() {
  136. paths = [];
  137. return '';
  138. },
  139. /**
  140. * Mark end of path. Actually serializes the path.
  141. * @return {string}
  142. */
  143. closePath: function() {
  144. var path = '';
  145. if (paths.length > 0) {
  146. path = '<path fill="none" \
  147. stroke="' + graphics.strokeStyle + '" \
  148. stroke-width="' + graphics.lineWidth + '" \
  149. stroke-linecap="' + graphics.lineCap + '" \
  150. stroke-linejoin="' + graphics.lineJoin + '" \
  151. stroke-miterlimit="' + graphics.miterLimit + '" \
  152. d="' + paths.join(' ') + '" />';
  153. paths = [];
  154. }
  155. return path;
  156. },
  157. /**
  158. * Add point origin to path.
  159. * @param {number} x - Horizontal coordinate.
  160. * @param {number} y - Vertical coordinate.
  161. * @return {string}
  162. */
  163. moveTo: function(x, y) {
  164. paths.push('M '+ x +' '+ y);
  165. return '';
  166. },
  167. /**
  168. * Add line to path.
  169. * @param {number} x - Horizontal coordinate.
  170. * @param {number} y - Vertical coordinate.
  171. * @return {string}
  172. */
  173. lineTo: function(x, y) {
  174. paths.push('L '+ x +' '+ y);
  175. return '';
  176. },
  177. /**
  178. * Add curve to path.
  179. * @param {number} cpx - Horizontal coordinate of control point.
  180. * @param {number} cpy - Vertical coordinate of control point.
  181. * @param {number} x - Horizontal coordinate.
  182. * @param {number} y - Vertical coordinate.
  183. * @return {string}
  184. */
  185. quadraticCurveTo: function(cpx, cpy, x, y) {
  186. paths.push('Q '+ cpx +' '+ cpy + ' '+ x +' '+ y);
  187. return '';
  188. },
  189. };
  190. // Create SVG.
  191. function build() {
  192. var svg = '<?xml version="1.0" encoding="utf-8" standalone="no" ?>';
  193. svg += '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" \
  194. "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">';
  195. svg += '<svg version="1.1" \
  196. xmlns="http://www.w3.org/2000/svg" \
  197. xmlns:svg="http://www.w3.org/2000/svg" \
  198. xmlns:xlink="http://www.w3.org/1999/xlink" \
  199. width="' + self.stageWidth + '" \
  200. height="' + self.stageHeight + '" \
  201. viewBox="0 0 ' + self.stageWidth + ' ' + self.stageHeight + '">';
  202. svg += '<desc>Generated with jSketch: https://luis.leiva.name/jsketch/</desc>';
  203. svg += '<defs></defs>';
  204. svg += '<g>';
  205. svg += processElements();
  206. svg += '</g>';
  207. svg += '</svg>';
  208. // Normalize whitespacing.
  209. return svg.replace(/\s+/g, ' ');
  210. }
  211. // Serialize jSketch elements.
  212. function processElements() {
  213. var ret = '';
  214. for (var i = 0; i < self.callStack.length; i++) {
  215. var entry = self.callStack[i];
  216. if (entry.property && typeof entry.value !== 'object') {
  217. // Save properties for later processing.
  218. graphics[entry.property] = entry.value;
  219. } else if (entry.method) {
  220. // Ensure method.
  221. if (!methods[entry.method]) {
  222. console.warn('Method not implemented:', entry.method);
  223. continue;
  224. }
  225. // Process method.
  226. ret += methods[entry.method].apply(null, entry.args);
  227. } else {
  228. console.warn('Unknown call:', entry);
  229. }
  230. }
  231. return ret;
  232. }
  233. // Throttle svg readiness.
  234. var timer = setInterval(checkReady, 150);
  235. function checkReady() {
  236. if (asyncOps <= 0) {
  237. clearInterval(timer);
  238. var contents = build();
  239. callback(contents);
  240. } else {
  241. console.info('Waiting for %s async operations to be finished ...', asyncOps);
  242. }
  243. }
  244. return this;
  245. };
  246. })(this);