@webwriter/flowchart
Version:
Create programming flowcharts with interactive tasks. Use standardized Elements such as loops and Branchings.
509 lines (478 loc) • 21.6 kB
text/typescript
import { GraphNode } from '../../definitions/GraphNode';
import { Arrow } from '../../definitions/Arrow';
import { getArrowInformation } from '../helper/arrowHelper';
import { getAnchors } from '../helper/anchorHelper';
export function drawArrow(
ctx: CanvasRenderingContext2D,
arrow: Arrow,
settings: { font: string; fontSize: number; theme: string },
isSelected: boolean = false,
selectedSequence: any[]
) {
const points = generateArrowPoints(ctx, arrow);
ctx.save();
applyArrowStyle(ctx, isSelected, arrow, selectedSequence);
ctx.beginPath();
ctx.setLineDash([]);
ctx.moveTo(points[0].x, points[0].y);
for (let i = 1; i < points.length; i++) {
ctx.lineTo(points[i].x, points[i].y);
}
ctx.stroke();
// Zeichne den Pfeilkopf
drawArrowHead(ctx, points[points.length - 1], points[points.length - 2]);
if (arrow.text) {
addArrowText(ctx, points, arrow.text, settings);
}
if (selectedSequence.length > 0) {
// Zeichnet die Zahlen basierend auf den berechneten Indizes.
// Bestimmt die Indizes, an denen die Pfeil-ID in der selectedSequence vorkommt.
const indices = selectedSequence
.map((item, index) => (item.id === arrow.id && item.type === 'arrow' ? index : -1))
.filter((index) => index !== -1);
indices.forEach((index, i) => {
const midPointIndex = Math.floor(points.length / 2 - 1);
const midPoint = {
x: (points[midPointIndex].x + points[midPointIndex + 1].x) / 2,
y: (points[midPointIndex].y + points[midPointIndex + 1].y) / 2,
};
ctx.stroke();
ctx.font = 'bold 16px Arial';
ctx.fillText((index + 1).toString(), midPoint.x + (i + 1) * 30, midPoint.y - i * 3);
});
}
ctx.restore();
}
export function generateArrowPoints(ctx: CanvasRenderingContext2D, arrow: Arrow) {
const fromArrowInfo = getArrowInformation(ctx, arrow.from, arrow.to, 'to');
const toArrowInfo = getArrowInformation(ctx, arrow.to, arrow.from, 'from');
const from = {
x: fromArrowInfo.x,
y: fromArrowInfo.y,
anchor: fromArrowInfo.anchor,
};
const to = {
x: toArrowInfo.x,
y: toArrowInfo.y,
anchor: toArrowInfo.anchor,
};
const padding = 15; // Raum zwischen Elementen und Pfeil
let points: { x: number; y: number }[] = [from];
if (from && to) {
// Halte die Positionen der Knoten zueinander fest
const isAbove = to.y < from.y;
const isLeft = to.x < from.x;
// Zum Zeichnen der Verbindungen zwischen zwei Knoten
// werden verschiedene Punkte in dem Array points festgehalten.
// Je nach Position zueinander sind die Punkte anders und müssen daher überprüft werden.
// SA - Startanker; ZA - Zielanker;
if (from.anchor === 0) {
// SA oben (0)
points.push({ x: from.x, y: from.y - padding });
if (to.anchor === 0) {
// SA oben -> ZA oben
if (isAbove) {
points.push({ x: from.x, y: to.y - padding });
points.push({ x: to.x, y: to.y - padding });
} else {
points.push({ x: to.x, y: from.y - padding });
}
} else if (to.anchor === 1) {
// SA oben -> ZA rechts
if (isAbove) {
if (isLeft) {
points.push({ x: from.x, y: to.y });
} else {
points.push({ x: from.x, y: (from.y + to.y) / 2 });
points.push({ x: to.x + padding, y: (from.y + to.y) / 2 });
points.push({ x: to.x + padding, y: to.y });
}
} else {
if (isLeft) {
points.push({ x: (from.x + to.x) / 2, y: from.y - padding });
points.push({ x: (from.x + to.x) / 2, y: to.y });
} else {
points.push({ x: to.x + padding, y: from.y - padding });
points.push({ x: to.x + padding, y: to.y });
}
}
} else if (to.anchor === 2) {
// SA oben -> ZA unten
if (from.x === to.x && isAbove) {
points.push({ x: from.x + padding, y: from.y - padding });
points.push({ x: from.x + padding, y: to.y + padding });
points.push({ x: to.x + padding, y: to.y + padding });
} else if (isAbove) {
points.pop();
points.push({ x: from.x, y: (from.y + to.y) / 2 });
points.push({ x: to.x, y: (from.y + to.y) / 2 });
} else {
points.push({ x: (from.x + to.x) / 2, y: from.y - padding });
points.push({ x: (from.x + to.x) / 2, y: to.y + padding });
points.push({ x: to.x, y: to.y + padding });
}
} else if (to.anchor === 3) {
// SA oben -> ZA links
if (isAbove) {
if (isLeft) {
points.push({ x: from.x, y: (from.y + to.y) / 2 });
points.push({ x: to.x - padding, y: (from.y + to.y) / 2 });
points.push({ x: to.x - padding, y: to.y });
} else {
points.push({ x: from.x, y: to.y });
}
} else {
if (isLeft) {
points.push({ x: to.x - padding, y: from.y - padding });
points.push({ x: to.x - padding, y: to.y });
} else {
points.push({ x: (from.x + to.x) / 2, y: from.y - padding });
points.push({ x: (from.x + to.x) / 2, y: to.y });
}
}
}
} else if (from.anchor === 1) {
// SA rechts (1)
points.push({ x: from.x + padding, y: from.y });
if (to.anchor === 0) {
// SA rechts -> ZA oben
if (isAbove) {
if (isLeft) {
points.push({ x: from.x + padding, y: to.y - padding });
points.push({ x: to.x, y: to.y - padding });
} else {
points.pop();
points.push({ x: (from.x + to.x) / 2, y: from.y });
points.push({ x: (from.x + to.x) / 2, y: to.y - padding });
points.push({ x: to.x, y: to.y - padding });
}
} else {
if (isLeft) {
points.push({ x: from.x + padding, y: to.y - padding });
points.push({ x: to.x, y: to.y - padding });
} else {
points.pop();
points.push({ x: to.x, y: from.y });
}
}
} else if (to.anchor === 1) {
// SA rechts -> ZA rechts
if (isLeft) {
points.push({ x: from.x + padding, y: to.y });
} else {
points.push({ x: to.x + padding, y: from.y });
points.push({ x: to.x + padding, y: to.y });
}
} else if (to.anchor === 2) {
// SA rechts -> ZA unten
if (isLeft) {
if (isAbove) {
points.push({ x: from.x + padding, y: (from.y + to.y) / 2 });
points.push({ x: to.x, y: (from.y + to.y) / 2 });
} else {
points.push({ x: from.x + padding, y: to.y + padding });
points.push({ x: to.x, y: to.y + padding });
}
} else {
points.pop();
if (isAbove) {
points.push({ x: to.x, y: from.y });
} else {
points.push({ x: (from.x + to.x) / 2, y: from.y });
points.push({ x: (from.x + to.x) / 2, y: to.y + padding });
points.push({ x: to.x, y: to.y + padding });
}
}
} else if (to.anchor === 3) {
// SA rechts -> ZA links
if (from.y === to.y && isLeft) {
points.push({ x: from.x + padding, y: from.y + padding });
points.push({ x: to.x - padding, y: from.y + padding });
points.push({ x: to.x - padding, y: to.y });
} else if (isLeft) {
points.push({ x: from.x + padding, y: (from.y + to.y) / 2 });
points.push({ x: to.x - padding, y: (from.y + to.y) / 2 });
points.push({ x: to.x - padding, y: to.y });
} else {
points.pop();
points.push({ x: (from.x + to.x) / 2, y: from.y });
points.push({ x: (from.x + to.x) / 2, y: to.y });
}
}
} else if (from.anchor === 2) {
// SA unten (2)
points.push({ x: from.x, y: from.y + padding });
if (to.anchor === 0) {
// SA unten -> ZA oben
if (from.x === to.x && isAbove) {
points.push({ x: from.x + padding, y: from.y + padding });
points.push({ x: to.x + padding, y: to.y - padding });
points.push({ x: to.x, y: to.y - padding });
} else if (isAbove) {
points.push({ x: (from.x + to.x) / 2, y: from.y + padding });
points.push({ x: (from.x + to.x) / 2, y: to.y - padding });
points.push({ x: to.x, y: to.y - padding });
} else {
points.pop();
points.push({ x: from.x, y: (from.y + to.y - 7) / 2 });
points.push({ x: to.x, y: (from.y + to.y - 7) / 2 });
}
} else if (to.anchor === 1) {
// SA unten -> ZA rechts
if (!isLeft) {
if (isAbove) {
points.push({ x: to.x + padding, y: from.y + padding });
points.push({ x: to.x + padding, y: to.y });
} else {
points.pop();
points.push({ x: from.x, y: (from.y + to.y) / 2 });
points.push({ x: to.x + padding, y: (from.y + to.y) / 2 });
points.push({ x: to.x + padding, y: to.y });
}
} else {
if (isAbove) {
points.push({ x: (from.x + to.x) / 2, y: from.y + padding });
points.push({ x: (from.x + to.x) / 2, y: to.y });
} else {
points.pop();
points.push({ x: from.x, y: to.y });
}
}
} else if (to.anchor === 2) {
// SA unten -> ZA unten
if (to.x === from.x && !isAbove) {
points.push({ x: from.x, y: to.y - 55 });
points.push({ x: from.x + padding, y: to.y - 55 });
points.push({ x: from.x + padding, y: to.y + padding });
points.push({ x: from.x, y: to.y + padding });
points.push({ x: to.x, y: to.y + padding });
} else if (to.x === from.x && isAbove) {
points.push({ x: from.x + padding, y: from.y + padding });
points.push({ x: from.x + padding, y: from.y - 55 });
points.push({ x: from.x, y: from.y - 55 });
} else if (isAbove) {
points.push({ x: to.x, y: from.y + padding });
} else {
points.push({ x: from.x, y: to.y + padding });
points.push({ x: to.x, y: to.y + padding });
}
} else if (to.anchor === 3) {
// SA unten -> ZA links
if (isLeft) {
if (isAbove) {
points.push({ x: to.x - padding, y: from.y + padding });
points.push({ x: to.x - padding, y: to.y });
} else {
points.pop();
points.push({ x: from.x, y: (from.y + to.y) / 2 });
points.push({ x: to.x - padding, y: (from.y + to.y) / 2 });
points.push({ x: to.x - padding, y: to.y });
}
} else {
if (isAbove) {
points.push({ x: (from.x + to.x) / 2, y: from.y + padding });
points.push({ x: (from.x + to.x) / 2, y: to.y });
} else {
points.pop();
points.push({ x: from.x, y: to.y });
}
}
}
} else if (from.anchor === 3) {
// SA links (3)
points.push({ x: from.x - padding, y: from.y });
if (to.anchor === 0) {
// SA links -> ZA oben
if (!isLeft) {
if (isAbove) {
points.push({ x: from.x - padding, y: to.y - padding });
points.push({ x: to.x, y: to.y - padding });
} else {
points.push({ x: from.x - padding, y: (from.y + to.y) / 2 });
points.push({ x: to.x, y: (from.y + to.y) / 2 });
}
} else {
if (isAbove) {
points.pop();
points.push({ x: (from.x + to.x) / 2, y: from.y });
points.push({ x: (from.x + to.x) / 2, y: to.y - padding });
points.push({ x: to.x, y: to.y - padding });
} else {
points.pop();
points.push({ x: to.x, y: from.y });
}
}
} else if (to.anchor === 1) {
// SA links -> ZA rechts
if (from.y === to.y && !isLeft) {
points.push({ x: from.x - padding, y: from.y + padding });
points.push({ x: to.x + padding, y: from.y + padding });
points.push({ x: to.x + padding, y: from.y });
} else if (isLeft) {
points.pop();
points.push({ x: (from.x + to.x) / 2, y: from.y });
points.push({ x: (from.x + to.x) / 2, y: to.y });
} else {
points.push({ x: from.x - padding, y: (from.y + to.y) / 2 });
points.push({ x: to.x + padding, y: (from.y + to.y) / 2 });
points.push({ x: to.x + padding, y: to.y });
}
} else if (to.anchor === 2) {
// SA links -> ZA unten
if (isAbove) {
if (isLeft) {
points.pop();
points.push({ x: to.x, y: from.y });
} else {
points.push({ x: from.x - padding, y: (from.y + to.y) / 2 });
points.push({ x: to.x, y: (from.y + to.y) / 2 });
}
} else {
if (isLeft) {
points.pop();
points.push({ x: (from.x + to.x) / 2, y: from.y });
points.push({ x: (from.x + to.x) / 2, y: to.y + padding });
points.push({ x: to.x, y: to.y + padding });
} else {
points.push({ x: from.x - padding, y: to.y + padding });
points.push({ x: to.x, y: to.y + padding });
}
}
} else if (to.anchor === 3) {
// SA links -> ZA links
if (isLeft) {
points.push({ x: to.x - padding, y: from.y });
points.push({ x: to.x - padding, y: to.y });
} else {
points.push({ x: from.x - padding, y: to.y });
}
}
} else {
// Setze keine Punkte beim temporären Pfeil oder unbekannten Anker
}
}
points.push(to);
return points;
}
function applyArrowStyle(ctx: CanvasRenderingContext2D, isSelected: boolean, arrow: Arrow, selectedSequence: any[]) {
ctx.strokeStyle = isSelected ? '#5CACEE' : 'black';
ctx.fillStyle = isSelected ? '#5CACEE' : 'black';
ctx.lineWidth = 2;
const isArrowIdInSelectedSequence = selectedSequence.some((item) => item.id === arrow.id && item.type === 'arrow');
if (isArrowIdInSelectedSequence) {
ctx.strokeStyle = '#990000';
ctx.fillStyle = '#990000';
}
}
function drawArrowHead(
ctx: CanvasRenderingContext2D,
toPoint: { x: number; y: number },
prevPoint: { x: number; y: number }
) {
const headLength = 7;
const angle = Math.atan2(toPoint.y - prevPoint.y, toPoint.x - prevPoint.x);
ctx.beginPath();
ctx.moveTo(toPoint.x, toPoint.y);
ctx.lineTo(
toPoint.x - headLength * Math.cos(angle - Math.PI / 6),
toPoint.y - headLength * Math.sin(angle - Math.PI / 6)
);
ctx.lineTo(
toPoint.x - headLength * Math.cos(angle + Math.PI / 6),
toPoint.y - headLength * Math.sin(angle + Math.PI / 6)
);
ctx.stroke();
ctx.fill();
ctx.closePath();
}
function addArrowText(
ctx: CanvasRenderingContext2D,
points: { x: number; y: number }[],
text: string,
settings: { font: string; fontSize: number; theme: string }
) {
const midPointIndex = Math.floor(points.length / 2 - 0.5);
const midPoint = {
x: (points[midPointIndex].x + points[midPointIndex + 1].x) / 2,
y: (points[midPointIndex].y + points[midPointIndex + 1].y) / 2,
};
ctx.save();
if (settings.font === 'Courier New') {
ctx.font = `bold ${settings.fontSize}px ${settings.font}`;
} else {
ctx.font = `${settings.fontSize}px ${settings.font}`;
}
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.translate(midPoint.x, midPoint.y);
ctx.fillStyle = 'white';
ctx.fillRect(-ctx.measureText(text).width / 2 - 5, -10, ctx.measureText(text).width + 10, 18);
ctx.fillStyle = 'black';
ctx.fillText(text, 0, 0);
ctx.restore();
}
// Zeichne die Ankerpunkte einer Verbindung
export function drawArrowAnchor(
ctx: CanvasRenderingContext2D,
arrow: Arrow,
ishovered: boolean,
settings: { font: string; fontSize: number; theme: string }
) {
const arrowInfo = getArrowInformation(ctx, arrow.to, arrow.from, 'from');
const anchor = arrowInfo.anchor;
const x = arrowInfo.x;
const y = arrowInfo.y;
const drawAnchor = (x: number, y: number, radius: number) => {
ctx.beginPath();
ctx.arc(x, y, radius, 0, 2 * Math.PI);
ctx.fillStyle = 'black';
ctx.fill();
ctx.closePath();
ctx.font = 'bold 12px Courier New';
ctx.textBaseline = 'middle';
ctx.textAlign = 'center';
ctx.fillStyle = 'white';
ctx.fillText('×', x, y);
};
// Falls ein Ankerpunkt gehovert wird, wird die Transparenz auf 1 gesetzt.
ishovered ? (ctx.globalAlpha = 1) : (ctx.globalAlpha = 0.6);
const offSetAnchor = 0;
switch (anchor) {
case 0:
drawAnchor(x, y - offSetAnchor, 5);
break;
case 1:
drawAnchor(x + offSetAnchor, y, 5);
break;
case 2:
drawAnchor(x, y + offSetAnchor, 5);
break;
case 3:
drawAnchor(x - offSetAnchor, y, 5);
break;
}
// Resette Einstellungen.
ctx.globalAlpha = 1;
if (settings.font === 'Courier New') {
ctx.font = `bold ${settings.fontSize}px ${settings.font}`;
} else {
ctx.font = `${settings.fontSize}px ${settings.font}`;
}
}
export function drawTempArrow(
ctx: CanvasRenderingContext2D,
arrowStart: { node: GraphNode; anchor: number },
tempArrowEnd?: { x: number; y: number }
) {
const arrowAnchors = getAnchors(ctx, arrowStart.node);
const startPoint = arrowAnchors[arrowStart.anchor];
const endPoint = tempArrowEnd;
ctx.beginPath();
ctx.fillStyle = 'black';
ctx.setLineDash([5, 10]);
ctx.moveTo(startPoint.x, startPoint.y);
ctx.lineTo(endPoint.x, endPoint.y);
ctx.stroke();
drawArrowHead(ctx, endPoint, startPoint);
ctx.setLineDash([]);
}