Платформа ЦРНП "Мирокод" для разработки проектов
https://git.mirocod.ru
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.
424 lines
12 KiB
424 lines
12 KiB
/* This is a customized version of https://github.com/bluef/gitgraph.js/blob/master/gitgraph.js |
|
Changes include conversion to ES6 and linting fixes */ |
|
|
|
/* |
|
* @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD 3-Clause |
|
* Copyright (c) 2011, Terrence Lee <kill889@gmail.com> |
|
* All rights reserved. |
|
* |
|
* Redistribution and use in source and binary forms, with or without |
|
* modification, are permitted provided that the following conditions are met: |
|
* * Redistributions of source code must retain the above copyright |
|
* notice, this list of conditions and the following disclaimer. |
|
* * Redistributions in binary form must reproduce the above copyright |
|
* notice, this list of conditions and the following disclaimer in the |
|
* documentation and/or other materials provided with the distribution. |
|
* * Neither the name of the <organization> nor the |
|
* names of its contributors may be used to endorse or promote products |
|
* derived from this software without specific prior written permission. |
|
* |
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND |
|
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
|
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
|
* DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY |
|
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
|
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND |
|
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
|
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
|
*/ |
|
|
|
export default function gitGraph(canvas, rawGraphList, config) { |
|
if (!canvas.getContext) { |
|
return; |
|
} |
|
|
|
if (typeof config === 'undefined') { |
|
config = { |
|
unitSize: 20, |
|
lineWidth: 3, |
|
nodeRadius: 4 |
|
}; |
|
} |
|
|
|
const flows = []; |
|
const graphList = []; |
|
|
|
const ctx = canvas.getContext('2d'); |
|
|
|
const devicePixelRatio = window.devicePixelRatio || 1; |
|
const backingStoreRatio = ctx.webkitBackingStorePixelRatio |
|
|| ctx.mozBackingStorePixelRatio |
|
|| ctx.msBackingStorePixelRatio |
|
|| ctx.oBackingStorePixelRatio |
|
|| ctx.backingStorePixelRatio || 1; |
|
|
|
const ratio = devicePixelRatio / backingStoreRatio; |
|
|
|
const init = function () { |
|
let maxWidth = 0; |
|
let i; |
|
const l = rawGraphList.length; |
|
let row; |
|
let midStr; |
|
|
|
for (i = 0; i < l; i++) { |
|
midStr = rawGraphList[i].replace(/\s+/g, ' ').replace(/^\s+|\s+$/g, ''); |
|
|
|
maxWidth = Math.max(midStr.replace(/(_|\s)/g, '').length, maxWidth); |
|
|
|
row = midStr.split(''); |
|
|
|
graphList.unshift(row); |
|
} |
|
|
|
const width = maxWidth * config.unitSize; |
|
const height = graphList.length * config.unitSize; |
|
|
|
canvas.width = width * ratio; |
|
canvas.height = height * ratio; |
|
|
|
canvas.style.width = `${width}px`; |
|
canvas.style.height = `${height}px`; |
|
|
|
ctx.lineWidth = config.lineWidth; |
|
ctx.lineJoin = 'round'; |
|
ctx.lineCap = 'round'; |
|
|
|
ctx.scale(ratio, ratio); |
|
}; |
|
|
|
const genRandomStr = function () { |
|
const chars = '0123456789ABCDEF'; |
|
const stringLength = 6; |
|
let randomString = '', rnum, i; |
|
for (i = 0; i < stringLength; i++) { |
|
rnum = Math.floor(Math.random() * chars.length); |
|
randomString += chars.substring(rnum, rnum + 1); |
|
} |
|
|
|
return randomString; |
|
}; |
|
|
|
const findFlow = function (id) { |
|
let i = flows.length; |
|
|
|
while (i-- && flows[i].id !== id); |
|
|
|
return i; |
|
}; |
|
|
|
const findColomn = function (symbol, row) { |
|
let i = row.length; |
|
|
|
while (i-- && row[i] !== symbol); |
|
|
|
return i; |
|
}; |
|
|
|
const findBranchOut = function (row) { |
|
if (!row) { |
|
return -1; |
|
} |
|
|
|
let i = row.length; |
|
|
|
while (i-- |
|
&& !(row[i - 1] && row[i] === '/' && row[i - 1] === '|') |
|
&& !(row[i - 2] && row[i] === '_' && row[i - 2] === '|')); |
|
|
|
return i; |
|
}; |
|
|
|
const findLineBreak = function (row) { |
|
if (!row) { |
|
return -1; |
|
} |
|
|
|
let i = row.length; |
|
|
|
while (i-- |
|
&& !(row[i - 1] && row[i - 2] && row[i] === ' ' && row[i - 1] === '|' && row[i - 2] === '_')); |
|
|
|
return i; |
|
}; |
|
|
|
const genNewFlow = function () { |
|
let newId; |
|
|
|
do { |
|
newId = genRandomStr(); |
|
} while (findFlow(newId) !== -1); |
|
|
|
return { id: newId, color: `#${newId}` }; |
|
}; |
|
|
|
// Draw methods |
|
const drawLine = function (moveX, moveY, lineX, lineY, color) { |
|
ctx.strokeStyle = color; |
|
ctx.beginPath(); |
|
ctx.moveTo(moveX, moveY); |
|
ctx.lineTo(lineX, lineY); |
|
ctx.stroke(); |
|
}; |
|
|
|
const drawLineRight = function (x, y, color) { |
|
drawLine(x, y + config.unitSize / 2, x + config.unitSize, y + config.unitSize / 2, color); |
|
}; |
|
|
|
const drawLineUp = function (x, y, color) { |
|
drawLine(x, y + config.unitSize / 2, x, y - config.unitSize / 2, color); |
|
}; |
|
|
|
const drawNode = function (x, y, color) { |
|
ctx.strokeStyle = color; |
|
|
|
drawLineUp(x, y, color); |
|
|
|
ctx.beginPath(); |
|
ctx.arc(x, y, config.nodeRadius, 0, Math.PI * 2, true); |
|
ctx.fill(); |
|
}; |
|
|
|
const drawLineIn = function (x, y, color) { |
|
drawLine(x + config.unitSize, y + config.unitSize / 2, x, y - config.unitSize / 2, color); |
|
}; |
|
|
|
const drawLineOut = function (x, y, color) { |
|
drawLine(x, y + config.unitSize / 2, x + config.unitSize, y - config.unitSize / 2, color); |
|
}; |
|
|
|
const draw = function (graphList) { |
|
let colomn, colomnIndex, prevColomn, condenseIndex, breakIndex = -1; |
|
let x, y; |
|
let color; |
|
let nodePos; |
|
let tempFlow; |
|
let prevRowLength = 0; |
|
let flowSwapPos = -1; |
|
let lastLinePos; |
|
let i, l; |
|
let condenseCurrentLength, condensePrevLength = 0; |
|
|
|
let inlineIntersect = false; |
|
|
|
// initiate color array for first row |
|
for (i = 0, l = graphList[0].length; i < l; i++) { |
|
if (graphList[0][i] !== '_' && graphList[0][i] !== ' ') { |
|
flows.push(genNewFlow()); |
|
} |
|
} |
|
|
|
y = (canvas.height / ratio) - config.unitSize * 0.5; |
|
|
|
// iterate |
|
for (i = 0, l = graphList.length; i < l; i++) { |
|
x = config.unitSize * 0.5; |
|
|
|
const currentRow = graphList[i]; |
|
const nextRow = graphList[i + 1]; |
|
const prevRow = graphList[i - 1]; |
|
|
|
flowSwapPos = -1; |
|
|
|
condenseCurrentLength = currentRow.filter((val) => { |
|
return (val !== ' ' && val !== '_'); |
|
}).length; |
|
|
|
// pre process begin |
|
// use last row for analysing |
|
if (prevRow) { |
|
if (!inlineIntersect) { |
|
// intersect might happen |
|
for (colomnIndex = 0; colomnIndex < prevRowLength; colomnIndex++) { |
|
if (prevRow[colomnIndex + 1] |
|
&& (prevRow[colomnIndex] === '/' && prevRow[colomnIndex + 1] === '|') |
|
|| ((prevRow[colomnIndex] === '_' && prevRow[colomnIndex + 1] === '|') |
|
&& (prevRow[colomnIndex + 2] === '/'))) { |
|
flowSwapPos = colomnIndex; |
|
|
|
// swap two flow |
|
tempFlow = { id: flows[flowSwapPos].id, color: flows[flowSwapPos].color }; |
|
|
|
flows[flowSwapPos].id = flows[flowSwapPos + 1].id; |
|
flows[flowSwapPos].color = flows[flowSwapPos + 1].color; |
|
|
|
flows[flowSwapPos + 1].id = tempFlow.id; |
|
flows[flowSwapPos + 1].color = tempFlow.color; |
|
} |
|
} |
|
} |
|
|
|
if (condensePrevLength < condenseCurrentLength |
|
&& ((nodePos = findColomn('*', currentRow)) !== -1 // eslint-disable-line no-cond-assign |
|
&& (findColomn('_', currentRow) === -1))) { |
|
flows.splice(nodePos - 1, 0, genNewFlow()); |
|
} |
|
|
|
if (prevRowLength > currentRow.length |
|
&& (nodePos = findColomn('*', prevRow)) !== -1) { // eslint-disable-line no-cond-assign |
|
if (findColomn('_', currentRow) === -1 |
|
&& findColomn('/', currentRow) === -1 |
|
&& findColomn('\\', currentRow) === -1) { |
|
flows.splice(nodePos + 1, 1); |
|
} |
|
} |
|
} // done with the previous row |
|
|
|
prevRowLength = currentRow.length; // store for next round |
|
colomnIndex = 0; // reset index |
|
condenseIndex = 0; |
|
condensePrevLength = 0; |
|
breakIndex = -1; // reset break index |
|
while (colomnIndex < currentRow.length) { |
|
colomn = currentRow[colomnIndex]; |
|
|
|
if (colomn !== ' ' && colomn !== '_') { |
|
++condensePrevLength; |
|
} |
|
|
|
// check and fix line break in next row |
|
if (colomn === '/' && currentRow[colomnIndex - 1] && currentRow[colomnIndex - 1] === '|') { |
|
/* eslint-disable-next-line */ |
|
if ((breakIndex = findLineBreak(nextRow)) !== -1) { |
|
nextRow.splice(breakIndex, 1); |
|
} |
|
} |
|
// if line break found replace all '/' with '|' after breakIndex in previous row |
|
if (breakIndex !== -1 && colomn === '/' && colomnIndex > breakIndex) { |
|
currentRow[colomnIndex] = '|'; |
|
colomn = '|'; |
|
} |
|
|
|
if (colomn === ' ' |
|
&& currentRow[colomnIndex + 1] |
|
&& currentRow[colomnIndex + 1] === '_' |
|
&& currentRow[colomnIndex - 1] |
|
&& currentRow[colomnIndex - 1] === '|') { |
|
currentRow.splice(colomnIndex, 1); |
|
|
|
currentRow[colomnIndex] = '/'; |
|
colomn = '/'; |
|
} |
|
|
|
// create new flow only when no intersect happened |
|
if (flowSwapPos === -1 |
|
&& colomn === '/' |
|
&& currentRow[colomnIndex - 1] |
|
&& currentRow[colomnIndex - 1] === '|') { |
|
flows.splice(condenseIndex, 0, genNewFlow()); |
|
} |
|
|
|
// change \ and / to | when it's in the last position of the whole row |
|
if (colomn === '/' || colomn === '\\') { |
|
if (!(colomn === '/' && findBranchOut(nextRow) === -1)) { |
|
/* eslint-disable-next-line */ |
|
if ((lastLinePos = Math.max(findColomn('|', currentRow), |
|
findColomn('*', currentRow))) !== -1 |
|
&& (lastLinePos < colomnIndex - 1)) { |
|
while (currentRow[++lastLinePos] === ' '); |
|
|
|
if (lastLinePos === colomnIndex) { |
|
currentRow[colomnIndex] = '|'; |
|
} |
|
} |
|
} |
|
} |
|
|
|
if (colomn === '*' |
|
&& prevRow |
|
&& prevRow[condenseIndex + 1] === '\\') { |
|
flows.splice(condenseIndex + 1, 1); |
|
} |
|
|
|
if (colomn !== ' ') { |
|
++condenseIndex; |
|
} |
|
|
|
++colomnIndex; |
|
} |
|
|
|
condenseCurrentLength = currentRow.filter((val) => { |
|
return (val !== ' ' && val !== '_'); |
|
}).length; |
|
|
|
// do some clean up |
|
if (flows.length > condenseCurrentLength) { |
|
flows.splice(condenseCurrentLength, flows.length - condenseCurrentLength); |
|
} |
|
|
|
colomnIndex = 0; |
|
|
|
// a little inline analysis and draw process |
|
while (colomnIndex < currentRow.length) { |
|
colomn = currentRow[colomnIndex]; |
|
prevColomn = currentRow[colomnIndex - 1]; |
|
|
|
if (currentRow[colomnIndex] === ' ') { |
|
currentRow.splice(colomnIndex, 1); |
|
x += config.unitSize; |
|
|
|
continue; |
|
} |
|
|
|
// inline interset |
|
if ((colomn === '_' || colomn === '/') |
|
&& currentRow[colomnIndex - 1] === '|' |
|
&& currentRow[colomnIndex - 2] === '_') { |
|
inlineIntersect = true; |
|
|
|
tempFlow = flows.splice(colomnIndex - 2, 1)[0]; |
|
flows.splice(colomnIndex - 1, 0, tempFlow); |
|
currentRow.splice(colomnIndex - 2, 1); |
|
|
|
colomnIndex -= 1; |
|
} else { |
|
inlineIntersect = false; |
|
} |
|
|
|
color = flows[colomnIndex].color; |
|
|
|
switch (colomn) { |
|
case '_': |
|
drawLineRight(x, y, color); |
|
|
|
x += config.unitSize; |
|
break; |
|
|
|
case '*': |
|
drawNode(x, y, color); |
|
break; |
|
|
|
case '|': |
|
drawLineUp(x, y, color); |
|
break; |
|
|
|
case '/': |
|
if (prevColomn |
|
&& (prevColomn === '/' |
|
|| prevColomn === ' ')) { |
|
x -= config.unitSize; |
|
} |
|
|
|
drawLineOut(x, y, color); |
|
|
|
x += config.unitSize; |
|
break; |
|
|
|
case '\\': |
|
drawLineIn(x, y, color); |
|
break; |
|
} |
|
|
|
++colomnIndex; |
|
} |
|
|
|
y -= config.unitSize; |
|
} |
|
}; |
|
|
|
init(); |
|
draw(graphList); |
|
} |
|
// @end-license
|
|
|