- Rahmenform Standard auf 'Rechteck' gesetzt (statt 'Keiner') - Pfeile werden nun korrekt im SVG-Bereich dargestellt (viewBox + Padding) - Numerische Eingabefelder neben allen Slidern hinzugefügt - Text-Positionierung im Rahmen ermöglicht (horizontal + vertikal) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
417 lines
15 KiB
JavaScript
417 lines
15 KiB
JavaScript
/**
|
|
* SVG Generator mit SVG.js
|
|
* Erzeugt Text-Symbole mit Rahmen und Pfeilen
|
|
*/
|
|
|
|
var SvgGenerator = {
|
|
|
|
getDashArray: function(style, weight) {
|
|
switch(style) {
|
|
case 'dashed': return (weight * 4) + ',' + (weight * 2);
|
|
case 'dotted': return weight + ',' + (weight * 1.5);
|
|
default: return null;
|
|
}
|
|
},
|
|
|
|
measureText: function(text, fontSize) {
|
|
var lines = text.split('\n');
|
|
var charWidth = fontSize * 0.6;
|
|
var lineHeight = fontSize * 1.3;
|
|
|
|
var maxLength = 0;
|
|
for (var i = 0; i < lines.length; i++) {
|
|
if (lines[i].length > maxLength) maxLength = lines[i].length;
|
|
}
|
|
if (maxLength === 0) maxLength = 4;
|
|
|
|
return {
|
|
lines: lines,
|
|
width: maxLength * charWidth,
|
|
height: lines.length * lineHeight,
|
|
lineHeight: lineHeight,
|
|
charWidth: charWidth
|
|
};
|
|
},
|
|
|
|
createShape: function(draw, shape, x, y, width, height, style) {
|
|
var shapeEl;
|
|
var halfW = width / 2;
|
|
var halfH = height / 2;
|
|
var cx = x + halfW;
|
|
var cy = y + halfH;
|
|
|
|
switch(shape) {
|
|
case 'rect':
|
|
shapeEl = draw.rect(width, height).move(x, y).radius(4);
|
|
break;
|
|
case 'square':
|
|
var squareSize = Math.max(width, height);
|
|
var squareX = x - (squareSize - width) / 2;
|
|
var squareY = y - (squareSize - height) / 2;
|
|
shapeEl = draw.rect(squareSize, squareSize).move(squareX, squareY).radius(4);
|
|
break;
|
|
case 'rounded':
|
|
shapeEl = draw.rect(width, height).move(x, y).radius(Math.min(halfW, halfH) * 0.5);
|
|
break;
|
|
case 'circle':
|
|
var circleRadius = Math.max(halfW, halfH);
|
|
shapeEl = draw.circle(circleRadius * 2).center(cx, cy);
|
|
break;
|
|
case 'oval':
|
|
shapeEl = draw.ellipse(width, height).center(cx, cy);
|
|
break;
|
|
case 'diamond':
|
|
shapeEl = draw.polygon([
|
|
[cx, y],
|
|
[x + width, cy],
|
|
[cx, y + height],
|
|
[x, cy]
|
|
]);
|
|
break;
|
|
default:
|
|
return null;
|
|
}
|
|
|
|
if (shapeEl) {
|
|
shapeEl.fill('none').stroke({ color: style.color, width: style.weight });
|
|
if (style.dashArray) {
|
|
shapeEl.attr('stroke-dasharray', style.dashArray);
|
|
}
|
|
}
|
|
return shapeEl;
|
|
},
|
|
|
|
calculateArrowHead: function(endX, endY, prevX, prevY, size, tipLength) {
|
|
// Berechne den Winkel der Pfeilrichtung
|
|
var angle = Math.atan2(endY - prevY, endX - prevX);
|
|
|
|
// Die zwei hinteren Punkte der Pfeilspitze:
|
|
// - tipLength zurueck in Pfeilrichtung
|
|
// - size/2 seitlich (links und rechts)
|
|
var baseX = endX - tipLength * Math.cos(angle);
|
|
var baseY = endY - tipLength * Math.sin(angle);
|
|
|
|
// Senkrecht zur Pfeilrichtung (90 Grad = PI/2)
|
|
var perpAngle = angle + Math.PI / 2;
|
|
var halfWidth = size / 2;
|
|
|
|
return [
|
|
[endX, endY], // Spitze
|
|
[baseX + halfWidth * Math.cos(perpAngle), baseY + halfWidth * Math.sin(perpAngle)], // Links
|
|
[baseX - halfWidth * Math.cos(perpAngle), baseY - halfWidth * Math.sin(perpAngle)] // Rechts
|
|
];
|
|
},
|
|
|
|
createArrow: function(draw, direction, frameRect, options) {
|
|
var length = options.length;
|
|
var angle = options.angle;
|
|
var bendPos = options.bendPos;
|
|
var color = options.color;
|
|
var weight = options.weight;
|
|
var dashArray = options.dashArray;
|
|
var arrowSize = options.arrowSize;
|
|
var tipLength = options.tipLength;
|
|
|
|
var angleRad = angle * Math.PI / 180;
|
|
var bend = bendPos / 100;
|
|
|
|
var startX, startY, midX, midY, endX, endY;
|
|
var fx = frameRect.x;
|
|
var fy = frameRect.y;
|
|
var fw = frameRect.width;
|
|
var fh = frameRect.height;
|
|
|
|
// Fuer Standalone-Pfeile (width=0, height=0) ist fx,fy der Startpunkt
|
|
var isStandalone = (fw === 0 && fh === 0);
|
|
var cx = isStandalone ? fx : fx + fw / 2;
|
|
var cy = isStandalone ? fy : fy + fh / 2;
|
|
|
|
switch(direction) {
|
|
case 'top':
|
|
startX = cx;
|
|
startY = isStandalone ? fy : fy;
|
|
midX = startX; midY = startY - length * bend;
|
|
endX = midX + Math.sin(angleRad) * (length * (1 - bend));
|
|
endY = midY - Math.cos(angleRad) * (length * (1 - bend));
|
|
break;
|
|
case 'bottom':
|
|
startX = cx;
|
|
startY = isStandalone ? fy : fy + fh;
|
|
midX = startX; midY = startY + length * bend;
|
|
endX = midX + Math.sin(angleRad) * (length * (1 - bend));
|
|
endY = midY + Math.cos(angleRad) * (length * (1 - bend));
|
|
break;
|
|
case 'left':
|
|
startX = isStandalone ? fx : fx;
|
|
startY = cy;
|
|
midX = startX - length * bend; midY = startY;
|
|
endX = midX - Math.cos(angleRad) * (length * (1 - bend));
|
|
endY = midY + Math.sin(angleRad) * (length * (1 - bend));
|
|
break;
|
|
case 'right':
|
|
startX = fx + fw; startY = cy;
|
|
midX = startX + length * bend; midY = startY;
|
|
endX = midX + Math.cos(angleRad) * (length * (1 - bend));
|
|
endY = midY + Math.sin(angleRad) * (length * (1 - bend));
|
|
break;
|
|
default:
|
|
return null;
|
|
}
|
|
|
|
var points = (angle !== 0 && bend > 0 && bend < 1)
|
|
? [[startX, startY], [midX, midY], [endX, endY]]
|
|
: [[startX, startY], [endX, endY]];
|
|
|
|
var pathData = '';
|
|
for (var i = 0; i < points.length; i++) {
|
|
pathData += (i === 0 ? 'M' : 'L') + points[i][0] + ' ' + points[i][1] + ' ';
|
|
}
|
|
|
|
var line = draw.path(pathData).fill('none').stroke({ color: color, width: weight });
|
|
if (dashArray) line.attr('stroke-dasharray', dashArray);
|
|
|
|
var lastPoint = points[points.length - 1];
|
|
var prevPoint = points[points.length - 2];
|
|
var headPoints = this.calculateArrowHead(lastPoint[0], lastPoint[1], prevPoint[0], prevPoint[1], arrowSize, tipLength);
|
|
var arrowHead = draw.polygon(headPoints).fill(color).stroke({ color: color, width: 1 });
|
|
|
|
return draw.group().add(line).add(arrowHead);
|
|
},
|
|
|
|
createText: function(draw, text, x, y, options) {
|
|
var fontSize = options.fontSize;
|
|
var color = options.color;
|
|
var lines = options.lines;
|
|
var lineHeight = options.lineHeight;
|
|
var alignX = options.alignX || 'center';
|
|
var alignY = options.alignY || 'center';
|
|
var frameWidth = options.frameWidth || 0;
|
|
var frameHeight = options.frameHeight || 0;
|
|
var group = draw.group();
|
|
|
|
var totalHeight = lines.length * lineHeight;
|
|
var startY;
|
|
|
|
// Vertikale Ausrichtung
|
|
switch(alignY) {
|
|
case 'top':
|
|
startY = y - frameHeight / 2 + fontSize * 0.8 + 5;
|
|
break;
|
|
case 'bottom':
|
|
startY = y + frameHeight / 2 - totalHeight + fontSize * 0.8 - 5;
|
|
break;
|
|
default: // center
|
|
startY = y - (totalHeight / 2) + (fontSize * 0.8);
|
|
}
|
|
|
|
// Text-Anchor fuer horizontale Ausrichtung
|
|
var anchor = alignX === 'left' ? 'start' : (alignX === 'right' ? 'end' : 'middle');
|
|
var textX = x;
|
|
if (alignX === 'left') {
|
|
textX = x - frameWidth / 2 + 5;
|
|
} else if (alignX === 'right') {
|
|
textX = x + frameWidth / 2 - 5;
|
|
}
|
|
|
|
for (var i = 0; i < lines.length; i++) {
|
|
var lineY = startY + (i * lineHeight);
|
|
var lineText = lines[i] || ' ';
|
|
var textEl = draw.plain(lineText)
|
|
.font({
|
|
family: 'Arial, sans-serif',
|
|
size: fontSize,
|
|
anchor: anchor
|
|
})
|
|
.fill(color)
|
|
.attr('x', textX)
|
|
.attr('y', lineY)
|
|
.attr('text-anchor', anchor);
|
|
group.add(textEl);
|
|
}
|
|
return group;
|
|
},
|
|
|
|
generate: function(state) {
|
|
var s = state;
|
|
var textMetrics = this.measureText(s.text || 'Text', s.fontSize);
|
|
|
|
var frameScale = s.frameScale / 100;
|
|
var frameWidth = (textMetrics.width + s.paddingLeft + s.paddingRight) * frameScale;
|
|
var frameHeight = (textMetrics.height + s.paddingTop + s.paddingBottom) * frameScale;
|
|
|
|
var minWidth = Math.max(frameWidth, 40);
|
|
var minHeight = Math.max(frameHeight, 30);
|
|
|
|
// Pfeil-Raum berechnen inkl. Winkel-Abweichung
|
|
var arrowSpace = 0;
|
|
var arrowSideSpace = 0;
|
|
if (s.arrow !== 'none') {
|
|
var angleRad = Math.abs(s.arrowAngle) * Math.PI / 180;
|
|
var bendFactor = s.arrowBend / 100;
|
|
var angledPart = s.arrowLength * (1 - bendFactor);
|
|
|
|
// Hauptrichtung: volle Laenge + Spitze + grosszuegiger Puffer
|
|
arrowSpace = s.arrowLength + s.arrowTipLength + s.arrowSize + 25;
|
|
|
|
// Seitliche Abweichung durch Winkel (beruecksichtige Pfeilspitze)
|
|
arrowSideSpace = Math.abs(Math.sin(angleRad) * angledPart) + s.arrowSize + s.arrowTipLength + 15;
|
|
}
|
|
|
|
var svgWidth = minWidth;
|
|
var svgHeight = minHeight;
|
|
var offsetX = 0, offsetY = 0;
|
|
|
|
switch(s.arrow) {
|
|
case 'left':
|
|
svgWidth += arrowSpace;
|
|
svgHeight = Math.max(svgHeight, minHeight + arrowSideSpace * 2);
|
|
offsetX = arrowSpace;
|
|
offsetY = (svgHeight - minHeight) / 2;
|
|
break;
|
|
case 'right':
|
|
svgWidth += arrowSpace;
|
|
svgHeight = Math.max(svgHeight, minHeight + arrowSideSpace * 2);
|
|
offsetY = (svgHeight - minHeight) / 2;
|
|
break;
|
|
case 'top':
|
|
svgHeight += arrowSpace;
|
|
svgWidth = Math.max(svgWidth, minWidth + arrowSideSpace * 2);
|
|
offsetY = arrowSpace;
|
|
offsetX = (svgWidth - minWidth) / 2;
|
|
break;
|
|
case 'bottom':
|
|
svgHeight += arrowSpace;
|
|
svgWidth = Math.max(svgWidth, minWidth + arrowSideSpace * 2);
|
|
offsetX = (svgWidth - minWidth) / 2;
|
|
break;
|
|
}
|
|
|
|
// Puffer um alle Elemente
|
|
var padding = 5;
|
|
svgWidth += padding * 2;
|
|
svgHeight += padding * 2;
|
|
offsetX += padding;
|
|
offsetY += padding;
|
|
|
|
var draw = SVG().size(svgWidth, svgHeight).viewbox(0, 0, svgWidth, svgHeight);
|
|
|
|
var frameX = offsetX;
|
|
var frameY = offsetY;
|
|
var frameCx = frameX + minWidth / 2;
|
|
var frameCy = frameY + minHeight / 2;
|
|
|
|
// Text-Verschiebung basierend auf asymmetrischem Padding
|
|
// Wenn links mehr Padding ist, verschiebt sich der Text nach rechts
|
|
var textOffsetX = (s.paddingLeft - s.paddingRight) / 2;
|
|
var textOffsetY = (s.paddingTop - s.paddingBottom) / 2;
|
|
var textCx = frameCx + textOffsetX;
|
|
var textCy = frameCy + textOffsetY;
|
|
|
|
var dashArray = this.getDashArray(s.lineStyle, s.lineWeight);
|
|
var strokeStyle = { color: s.frameColor, weight: s.lineWeight, dashArray: dashArray };
|
|
|
|
if (s.shape !== 'none') {
|
|
this.createShape(draw, s.shape, frameX, frameY, minWidth, minHeight, strokeStyle);
|
|
}
|
|
|
|
if (s.arrow !== 'none') {
|
|
this.createArrow(draw, s.arrow, {
|
|
x: frameX, y: frameY, width: minWidth, height: minHeight
|
|
}, {
|
|
length: s.arrowLength, angle: s.arrowAngle, bendPos: s.arrowBend,
|
|
color: s.frameColor, weight: s.lineWeight, dashArray: dashArray,
|
|
arrowSize: s.arrowSize, tipLength: s.arrowTipLength
|
|
});
|
|
}
|
|
|
|
this.createText(draw, s.text, textCx, textCy, {
|
|
fontSize: s.fontSize, color: s.textColor,
|
|
lines: textMetrics.lines, lineHeight: textMetrics.lineHeight,
|
|
alignX: s.textAlignX || 'center',
|
|
alignY: s.textAlignY || 'center',
|
|
frameWidth: minWidth,
|
|
frameHeight: minHeight
|
|
});
|
|
|
|
return draw.svg();
|
|
},
|
|
|
|
generateArrowOnly: function(state) {
|
|
var s = state;
|
|
if (s.arrow === 'none') return null;
|
|
|
|
var length = s.arrowLength;
|
|
var tipLength = s.arrowTipLength;
|
|
var arrowSize = s.arrowSize;
|
|
var padding = 40;
|
|
|
|
// Berechne die tatsaechlichen Endpunkte des Pfeils
|
|
var angleRad = s.arrowAngle * Math.PI / 180;
|
|
var bendFactor = s.arrowBend / 100;
|
|
var straightPart = length * bendFactor;
|
|
var angledPart = length * (1 - bendFactor);
|
|
|
|
// Seitliche Ausdehnung durch den Winkel (mit Vorzeichen)
|
|
var sideExtent = Math.sin(angleRad) * angledPart;
|
|
var mainExtent = Math.cos(angleRad) * angledPart;
|
|
|
|
// Mindestgroesse fuer die seitliche Ausdehnung
|
|
var minSideSpace = Math.abs(sideExtent) + arrowSize + tipLength + padding;
|
|
|
|
var width, height, startX, startY;
|
|
|
|
switch(s.arrow) {
|
|
case 'right':
|
|
width = straightPart + mainExtent + tipLength + padding * 2;
|
|
height = Math.max(arrowSize * 4, minSideSpace * 2);
|
|
startX = padding;
|
|
// Bei positivem Winkel geht der Pfeil nach unten, also Start weiter oben
|
|
startY = height / 2 - sideExtent / 2;
|
|
break;
|
|
case 'left':
|
|
width = straightPart + mainExtent + tipLength + padding * 2;
|
|
height = Math.max(arrowSize * 4, minSideSpace * 2);
|
|
startX = width - padding;
|
|
startY = height / 2 + sideExtent / 2;
|
|
break;
|
|
case 'bottom':
|
|
width = Math.max(arrowSize * 4, minSideSpace * 2);
|
|
height = straightPart + mainExtent + tipLength + padding * 2;
|
|
startX = width / 2 - sideExtent / 2;
|
|
startY = padding;
|
|
break;
|
|
case 'top':
|
|
width = Math.max(arrowSize * 4, minSideSpace * 2);
|
|
height = straightPart + mainExtent + tipLength + padding * 2;
|
|
startX = width / 2 + sideExtent / 2;
|
|
startY = height - padding;
|
|
break;
|
|
}
|
|
|
|
// Stelle sicher, dass alle Werte positiv sind und genuegend Platz haben
|
|
width = Math.max(width, 100);
|
|
height = Math.max(height, 80);
|
|
|
|
var draw = SVG().size(width, height).viewbox(0, 0, width, height);
|
|
|
|
// Simuliere einen Rahmen-Punkt als Startpunkt
|
|
var frameRect = {
|
|
x: startX,
|
|
y: startY,
|
|
width: 0,
|
|
height: 0
|
|
};
|
|
|
|
this.createArrow(draw, s.arrow, frameRect, {
|
|
length: s.arrowLength, angle: s.arrowAngle, bendPos: s.arrowBend,
|
|
color: s.frameColor, weight: s.lineWeight,
|
|
dashArray: this.getDashArray(s.lineStyle, s.lineWeight),
|
|
arrowSize: s.arrowSize, tipLength: s.arrowTipLength
|
|
});
|
|
|
|
return draw.svg();
|
|
}
|
|
};
|
|
|
|
window.SvgGenerator = SvgGenerator;
|