You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

gitgraph.custom.js 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. /*
  2. * @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD 3-Clause
  3. * Copyright (c) 2011, Terrence Lee <kill889@gmail.com>
  4. * All rights reserved.
  5. *
  6. * Redistribution and use in source and binary forms, with or without
  7. * modification, are permitted provided that the following conditions are met:
  8. * * Redistributions of source code must retain the above copyright
  9. * notice, this list of conditions and the following disclaimer.
  10. * * Redistributions in binary form must reproduce the above copyright
  11. * notice, this list of conditions and the following disclaimer in the
  12. * documentation and/or other materials provided with the distribution.
  13. * * Neither the name of the <organization> nor the
  14. * names of its contributors may be used to endorse or promote products
  15. * derived from this software without specific prior written permission.
  16. *
  17. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  18. * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  19. * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  20. * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
  21. * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  22. * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  23. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  24. * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  25. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  26. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  27. */
  28. export default function gitGraph(canvas, rawGraphList, config) {
  29. if (!canvas.getContext) {
  30. return;
  31. }
  32. if (typeof config === 'undefined') {
  33. config = {
  34. unitSize: 20,
  35. lineWidth: 3,
  36. nodeRadius: 4
  37. };
  38. }
  39. const flows = [];
  40. const graphList = [];
  41. const ctx = canvas.getContext('2d');
  42. const devicePixelRatio = window.devicePixelRatio || 1;
  43. const backingStoreRatio = ctx.webkitBackingStorePixelRatio
  44. || ctx.mozBackingStorePixelRatio
  45. || ctx.msBackingStorePixelRatio
  46. || ctx.oBackingStorePixelRatio
  47. || ctx.backingStorePixelRatio || 1;
  48. const ratio = devicePixelRatio / backingStoreRatio;
  49. const init = function () {
  50. let maxWidth = 0;
  51. let i;
  52. const l = rawGraphList.length;
  53. let row;
  54. let midStr;
  55. for (i = 0; i < l; i++) {
  56. midStr = rawGraphList[i].replace(/\s+/g, ' ').replace(/^\s+|\s+$/g, '');
  57. maxWidth = Math.max(midStr.replace(/(_|\s)/g, '').length, maxWidth);
  58. row = midStr.split('');
  59. graphList.unshift(row);
  60. }
  61. const width = maxWidth * config.unitSize;
  62. const height = graphList.length * config.unitSize;
  63. canvas.width = width * ratio;
  64. canvas.height = height * ratio;
  65. canvas.style.width = `${width}px`;
  66. canvas.style.height = `${height}px`;
  67. ctx.lineWidth = config.lineWidth;
  68. ctx.lineJoin = 'round';
  69. ctx.lineCap = 'round';
  70. ctx.scale(ratio, ratio);
  71. };
  72. const genRandomStr = function () {
  73. const chars = '0123456789ABCDEF';
  74. const stringLength = 6;
  75. let randomString = '', rnum, i;
  76. for (i = 0; i < stringLength; i++) {
  77. rnum = Math.floor(Math.random() * chars.length);
  78. randomString += chars.substring(rnum, rnum + 1);
  79. }
  80. return randomString;
  81. };
  82. const findFlow = function (id) {
  83. let i = flows.length;
  84. while (i-- && flows[i].id !== id);
  85. return i;
  86. };
  87. const findColomn = function (symbol, row) {
  88. let i = row.length;
  89. while (i-- && row[i] !== symbol);
  90. return i;
  91. };
  92. const findBranchOut = function (row) {
  93. if (!row) {
  94. return -1;
  95. }
  96. let i = row.length;
  97. while (i--
  98. && !(row[i - 1] && row[i] === '/' && row[i - 1] === '|')
  99. && !(row[i - 2] && row[i] === '_' && row[i - 2] === '|'));
  100. return i;
  101. };
  102. const findLineBreak = function (row) {
  103. if (!row) {
  104. return -1;
  105. }
  106. let i = row.length;
  107. while (i--
  108. && !(row[i - 1] && row[i - 2] && row[i] === ' ' && row[i - 1] === '|' && row[i - 2] === '_'));
  109. return i;
  110. };
  111. const genNewFlow = function () {
  112. let newId;
  113. do {
  114. newId = genRandomStr();
  115. } while (findFlow(newId) !== -1);
  116. return { id: newId, color: `#${newId}` };
  117. };
  118. // Draw methods
  119. const drawLine = function (moveX, moveY, lineX, lineY, color) {
  120. ctx.strokeStyle = color;
  121. ctx.beginPath();
  122. ctx.moveTo(moveX, moveY);
  123. ctx.lineTo(lineX, lineY);
  124. ctx.stroke();
  125. };
  126. const drawLineRight = function (x, y, color) {
  127. drawLine(x, y + config.unitSize / 2, x + config.unitSize, y + config.unitSize / 2, color);
  128. };
  129. const drawLineUp = function (x, y, color) {
  130. drawLine(x, y + config.unitSize / 2, x, y - config.unitSize / 2, color);
  131. };
  132. const drawNode = function (x, y, color) {
  133. ctx.strokeStyle = color;
  134. drawLineUp(x, y, color);
  135. ctx.beginPath();
  136. ctx.arc(x, y, config.nodeRadius, 0, Math.PI * 2, true);
  137. ctx.fill();
  138. };
  139. const drawLineIn = function (x, y, color) {
  140. drawLine(x + config.unitSize, y + config.unitSize / 2, x, y - config.unitSize / 2, color);
  141. };
  142. const drawLineOut = function (x, y, color) {
  143. drawLine(x, y + config.unitSize / 2, x + config.unitSize, y - config.unitSize / 2, color);
  144. };
  145. const draw = function (graphList) {
  146. let colomn, colomnIndex, prevColomn, condenseIndex, breakIndex = -1;
  147. let x, y;
  148. let color;
  149. let nodePos;
  150. let tempFlow;
  151. let prevRowLength = 0;
  152. let flowSwapPos = -1;
  153. let lastLinePos;
  154. let i, l;
  155. let condenseCurrentLength, condensePrevLength = 0;
  156. let inlineIntersect = false;
  157. // initiate color array for first row
  158. for (i = 0, l = graphList[0].length; i < l; i++) {
  159. if (graphList[0][i] !== '_' && graphList[0][i] !== ' ') {
  160. flows.push(genNewFlow());
  161. }
  162. }
  163. y = (canvas.height / ratio) - config.unitSize * 0.5;
  164. // iterate
  165. for (i = 0, l = graphList.length; i < l; i++) {
  166. x = config.unitSize * 0.5;
  167. const currentRow = graphList[i];
  168. const nextRow = graphList[i + 1];
  169. const prevRow = graphList[i - 1];
  170. flowSwapPos = -1;
  171. condenseCurrentLength = currentRow.filter((val) => {
  172. return (val !== ' ' && val !== '_');
  173. }).length;
  174. // pre process begin
  175. // use last row for analysing
  176. if (prevRow) {
  177. if (!inlineIntersect) {
  178. // intersect might happen
  179. for (colomnIndex = 0; colomnIndex < prevRowLength; colomnIndex++) {
  180. if (prevRow[colomnIndex + 1]
  181. && (prevRow[colomnIndex] === '/' && prevRow[colomnIndex + 1] === '|')
  182. || ((prevRow[colomnIndex] === '_' && prevRow[colomnIndex + 1] === '|')
  183. && (prevRow[colomnIndex + 2] === '/'))) {
  184. flowSwapPos = colomnIndex;
  185. // swap two flow
  186. tempFlow = { id: flows[flowSwapPos].id, color: flows[flowSwapPos].color };
  187. flows[flowSwapPos].id = flows[flowSwapPos + 1].id;
  188. flows[flowSwapPos].color = flows[flowSwapPos + 1].color;
  189. flows[flowSwapPos + 1].id = tempFlow.id;
  190. flows[flowSwapPos + 1].color = tempFlow.color;
  191. }
  192. }
  193. }
  194. if (condensePrevLength < condenseCurrentLength
  195. && ((nodePos = findColomn('*', currentRow)) !== -1
  196. && (findColomn('_', currentRow) === -1))) {
  197. flows.splice(nodePos - 1, 0, genNewFlow());
  198. }
  199. if (prevRowLength > currentRow.length
  200. && (nodePos = findColomn('*', prevRow)) !== -1) {
  201. if (findColomn('_', currentRow) === -1
  202. && findColomn('/', currentRow) === -1
  203. && findColomn('\\', currentRow) === -1) {
  204. flows.splice(nodePos + 1, 1);
  205. }
  206. }
  207. } // done with the previous row
  208. prevRowLength = currentRow.length; // store for next round
  209. colomnIndex = 0; // reset index
  210. condenseIndex = 0;
  211. condensePrevLength = 0;
  212. breakIndex = -1; // reset break index
  213. while (colomnIndex < currentRow.length) {
  214. colomn = currentRow[colomnIndex];
  215. if (colomn !== ' ' && colomn !== '_') {
  216. ++condensePrevLength;
  217. }
  218. // check and fix line break in next row
  219. if (colomn === '/' && currentRow[colomnIndex - 1] && currentRow[colomnIndex - 1] === '|') {
  220. if ((breakIndex = findLineBreak(nextRow)) !== -1) {
  221. nextRow.splice(breakIndex, 1);
  222. }
  223. }
  224. // if line break found replace all '/' with '|' after breakIndex in previous row
  225. if (breakIndex !== -1 && colomn === '/' && colomnIndex > breakIndex) {
  226. currentRow[colomnIndex] = '|';
  227. colomn = '|';
  228. }
  229. if (colomn === ' '
  230. && currentRow[colomnIndex + 1]
  231. && currentRow[colomnIndex + 1] === '_'
  232. && currentRow[colomnIndex - 1]
  233. && currentRow[colomnIndex - 1] === '|') {
  234. currentRow.splice(colomnIndex, 1);
  235. currentRow[colomnIndex] = '/';
  236. colomn = '/';
  237. }
  238. // create new flow only when no intersect happened
  239. if (flowSwapPos === -1
  240. && colomn === '/'
  241. && currentRow[colomnIndex - 1]
  242. && currentRow[colomnIndex - 1] === '|') {
  243. flows.splice(condenseIndex, 0, genNewFlow());
  244. }
  245. // change \ and / to | when it's in the last position of the whole row
  246. if (colomn === '/' || colomn === '\\') {
  247. if (!(colomn === '/' && findBranchOut(nextRow) === -1)) {
  248. if ((lastLinePos = Math.max(findColomn('|', currentRow),
  249. findColomn('*', currentRow))) !== -1
  250. && (lastLinePos < colomnIndex - 1)) {
  251. while (currentRow[++lastLinePos] === ' ');
  252. if (lastLinePos === colomnIndex) {
  253. currentRow[colomnIndex] = '|';
  254. }
  255. }
  256. }
  257. }
  258. if (colomn === '*'
  259. && prevRow
  260. && prevRow[condenseIndex + 1] === '\\') {
  261. flows.splice(condenseIndex + 1, 1);
  262. }
  263. if (colomn !== ' ') {
  264. ++condenseIndex;
  265. }
  266. ++colomnIndex;
  267. }
  268. condenseCurrentLength = currentRow.filter((val) => {
  269. return (val !== ' ' && val !== '_');
  270. }).length;
  271. // do some clean up
  272. if (flows.length > condenseCurrentLength) {
  273. flows.splice(condenseCurrentLength, flows.length - condenseCurrentLength);
  274. }
  275. colomnIndex = 0;
  276. // a little inline analysis and draw process
  277. while (colomnIndex < currentRow.length) {
  278. colomn = currentRow[colomnIndex];
  279. prevColomn = currentRow[colomnIndex - 1];
  280. if (currentRow[colomnIndex] === ' ') {
  281. currentRow.splice(colomnIndex, 1);
  282. x += config.unitSize;
  283. continue;
  284. }
  285. // inline interset
  286. if ((colomn === '_' || colomn === '/')
  287. && currentRow[colomnIndex - 1] === '|'
  288. && currentRow[colomnIndex - 2] === '_') {
  289. inlineIntersect = true;
  290. tempFlow = flows.splice(colomnIndex - 2, 1)[0];
  291. flows.splice(colomnIndex - 1, 0, tempFlow);
  292. currentRow.splice(colomnIndex - 2, 1);
  293. colomnIndex -= 1;
  294. } else {
  295. inlineIntersect = false;
  296. }
  297. color = flows[colomnIndex].color;
  298. switch (colomn) {
  299. case '_':
  300. drawLineRight(x, y, color);
  301. x += config.unitSize;
  302. break;
  303. case '*':
  304. drawNode(x, y, color);
  305. break;
  306. case '|':
  307. drawLineUp(x, y, color);
  308. break;
  309. case '/':
  310. if (prevColomn
  311. && (prevColomn === '/'
  312. || prevColomn === ' ')) {
  313. x -= config.unitSize;
  314. }
  315. drawLineOut(x, y, color);
  316. x += config.unitSize;
  317. break;
  318. case '\\':
  319. drawLineIn(x, y, color);
  320. break;
  321. }
  322. ++colomnIndex;
  323. }
  324. y -= config.unitSize;
  325. }
  326. };
  327. init();
  328. draw(graphList);
  329. }
  330. // @end-license