SVG dynamic positioning and dimensions - javascript

I have a web app that renders svg elements from cartesian points (lines, paths, etc) that come from a database.
I have a requirement that an end user can upload an svg file (icons) and drag the icon to fit within specific bounds of the points already defined and rendered in the app.
For example (see snippet), a user can upload the 'x' icon and drag it near the green line defined by two points, which should result in the icon being snapped and resized to the line - the upper left corner snapped to the line start point, and the width of the icon extending to the line end point. Same is true for the file icon being snapped to the red line. This is done dynamically during drag with js. I have omitted the js from the snippet to keep things simple, as I am confident that the answer lies with svg attributes and or style that I can set with js, but the svg properties/values are what I cannot pin down.
What I have tried - everything, I think. Given that I am nesting svg elements, I took the BBox values as an offset to used the x and y attributes on the icon svg element, and that moved it, but not to the start point. I also tried translate without success. I am able to move and resize, but not to the coordinates I need. I do not want to change the icon svg at all if possible, so i'd prefer to leave its viewBox as-is.
<svg height="700" width="700" fill="#e6e6e6" xmlns="http://www.w3.org/2000/svg">
<svg viewBox="0 0 512 512">
<path d="M443.6,387.1L312.4,255.4l131.5-130c5.4-5.4,5.4-14.2,0-19.6l-37.4-37.6c-2.6-2.6-6.1-4-9.8-4c-3.7,0-7.2,1.5-9.8,4 L256,197.8L124.9,68.3c-2.6-2.6-6.1-4-9.8-4c-3.7,0-7.2,1.5-9.8,4L68,105.9c-5.4,5.4-5.4,14.2,0,19.6l131.5,130L68.4,387.1 c-2.6,2.6-4.1,6.1-4.1,9.8c0,3.7,1.4,7.2,4.1,9.8l37.4,37.6c2.7,2.7,6.2,4.1,9.8,4.1c3.5,0,7.1-1.3,9.8-4.1L256,313.1l130.7,131.1 c2.7,2.7,6.2,4.1,9.8,4.1c3.5,0,7.1-1.3,9.8-4.1l37.4-37.6c2.6-2.6,4.1-6.1,4.1-9.8C447.7,393.2,446.2,389.7,443.6,387.1z"/>
</svg>
<svg viewBox="0 0 380 511.7">
<path fill-rule="nonzero" d="M26.18 0h221.14c3.1 0 5.85 1.51 7.56 3.84l122.88 145.08a9.27 9.27 0 0 1 2.21 6.05l.03 330.55c0 7.13-2.98 13.68-7.72 18.42l-.03.04c-4.75 4.74-11.29 7.72-18.43 7.72H26.18c-7.13 0-13.69-2.96-18.45-7.71l-.03-.04C2.97 499.22 0 492.69 0 485.52V26.18C0 19 2.95 12.46 7.68 7.72l.04-.04C12.46 2.95 19 0 26.18 0zm335.06 164.7c-134.78-5.58-134.35-17.38-129.82-134.02l.45-11.92H26.18c-2.05 0-3.91.83-5.26 2.16a7.482 7.482 0 0 0-2.16 5.26v459.34c0 2.02.84 3.88 2.18 5.23 1.36 1.35 3.22 2.19 5.24 2.19h327.64c2.01 0 3.86-.85 5.22-2.2 1.35-1.36 2.2-3.21 2.2-5.22V164.7zM250.25 27.32l-.15 4.01c-3.73 96.04-4.22 109.01 100.23 114.16L250.25 27.32z"/>
</svg>
<line x1="100" y1="20" x2="200" y2="20" stroke="green" />
<line x1="300" y1="20" x2="350" y2="20" stroke="red" />
</svg>
strong text

Although there may be several ways to accomplish this, I figured out one way that I am going to move forward with. The confusion with this was primarily due to my lack of understanding of the svg viewBox and how the coordinate system works.
The key to this is defining a new viewBox value for each nested svg based on its BBox coordinates. The purpose of doing this is to frame the drawing without 'whitespace' to enable its placement at the desired coordinates. Once you have the BBox data, you can set the viewBox and do some simple math to properly set the desired height and width (both of which must be defined for the nested svg). After updating the viewBox, width, and height, you can then move the svg to the new location.
$( document ).ready(function() {
const svg = document.querySelectorAll('svg.a');
svg.forEach(x => {
setSvg(x);
});
});
function setSvg(svg){
const { xMin, xMax, yMin, yMax } = [...svg.children].reduce((acc, el) => {
const { x, y, width, height } = el.getBBox();
if (!acc.xMin || x < acc.xMin) acc.xMin = x;
if (!acc.xMax || x + width > acc.xMax) acc.xMax = x + width;
if (!acc.yMin || y < acc.yMin) acc.yMin = y;
if (!acc.yMax || y + height > acc.yMax) acc.yMax = y + height;
return acc;
}, {});
const viewbox = `${xMin} ${yMin} ${xMax - xMin} ${yMax - yMin}`;
let newWidth = $(svg).attr('data-new-width');
let newPosition = $(svg).attr('data-new-position');
let newHeight = newWidth*(yMax - yMin)/(xMax - xMin);
svg.setAttribute('width', newWidth);
svg.setAttribute('height', newHeight);
svg.setAttribute('x', newPosition.split(",")[0]);
svg.setAttribute('y', newPosition.split(",")[1]);
svg.setAttribute('viewBox', viewbox);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<svg height="700" width="700" fill="#e6e6e6" xmlns="http://www.w3.org/2000/svg">
<svg class="a" data-new-width="100" data-new-position="100,20" viewBox="0 0 512 512">
<path d="M443.6,387.1L312.4,255.4l131.5-130c5.4-5.4,5.4-14.2,0-19.6l-37.4-37.6c-2.6-2.6-6.1-4-9.8-4c-3.7,0-7.2,1.5-9.8,4 L256,197.8L124.9,68.3c-2.6-2.6-6.1-4-9.8-4c-3.7,0-7.2,1.5-9.8,4L68,105.9c-5.4,5.4-5.4,14.2,0,19.6l131.5,130L68.4,387.1 c-2.6,2.6-4.1,6.1-4.1,9.8c0,3.7,1.4,7.2,4.1,9.8l37.4,37.6c2.7,2.7,6.2,4.1,9.8,4.1c3.5,0,7.1-1.3,9.8-4.1L256,313.1l130.7,131.1 c2.7,2.7,6.2,4.1,9.8,4.1c3.5,0,7.1-1.3,9.8-4.1l37.4-37.6c2.6-2.6,4.1-6.1,4.1-9.8C447.7,393.2,446.2,389.7,443.6,387.1z"/>
</svg>
<svg class="a" data-new-width="50" data-new-position="300,20" viewBox="0 0 380 511.7">
<path fill-rule="nonzero" d="M26.18 0h221.14c3.1 0 5.85 1.51 7.56 3.84l122.88 145.08a9.27 9.27 0 0 1 2.21 6.05l.03 330.55c0 7.13-2.98 13.68-7.72 18.42l-.03.04c-4.75 4.74-11.29 7.72-18.43 7.72H26.18c-7.13 0-13.69-2.96-18.45-7.71l-.03-.04C2.97 499.22 0 492.69 0 485.52V26.18C0 19 2.95 12.46 7.68 7.72l.04-.04C12.46 2.95 19 0 26.18 0zm335.06 164.7c-134.78-5.58-134.35-17.38-129.82-134.02l.45-11.92H26.18c-2.05 0-3.91.83-5.26 2.16a7.482 7.482 0 0 0-2.16 5.26v459.34c0 2.02.84 3.88 2.18 5.23 1.36 1.35 3.22 2.19 5.24 2.19h327.64c2.01 0 3.86-.85 5.22-2.2 1.35-1.36 2.2-3.21 2.2-5.22V164.7zM250.25 27.32l-.15 4.01c-3.73 96.04-4.22 109.01 100.23 114.16L250.25 27.32z"/>
</svg>
<line x1="100" y1="20" x2="200" y2="20" stroke="green" />
<line x1="300" y1="20" x2="350" y2="20" stroke="red" />
</svg>

Related

Merge multiple SVG elements into one SVG and download it with React

I have a logo creator in my app.
User has to type smthn in input field (for example "TESTING") and then it renders in preview block.
Every letter in preview is the svg element:
(Each letter can be a glyph letter or regular letter, it means that there are 2 different fonts)
const C = ({ glyph }) => {
if (!glyph) {
return (
<svg width="93" height="132" viewBox="0 0 93 132" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M75 41.76L87.6 34.92C82.2 21.6 67.2 10.8 49.08 10.8C19.92 10.8 4.8 30.12 4.8 54C4.8 77.88 19.2 97.2 49.08 97.2C67.2 97.2 82.2 86.4 87.6 73.08L75 66.12C71.04 75.72 61.92 84 48.48 84C30.72 84 19.68 71.76 19.68 54C19.68 36.24 30.72 24 48.48 24C61.92 24 71.04 32.28 75 41.76Z"
fill="currentColor"
/>
</svg>
);
} else {
return (
<svg width="96" height="132" viewBox="0 0 96 132" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M20.4 51.24H33.6C33.6 31.92 42.24 24 54.36 24C64.92 24 72.36 29.16 76.2 40.56L88.8 33.6C83.4 19.2 71.64 10.8 54.48 10.8C32.04 10.8 18.72 25.08 18.72 51.24H4.8C4.8 80.52 24.48 97.08 49.8 97.08C70.68 97.08 83.04 85.8 89.16 74.04L77.64 67.44C71.88 77.16 64.2 83.64 50.4 83.64C33.12 83.64 21.12 71.16 20.4 51.24Z"
fill="currentColor"
/>
</svg>
);
}
};
I change glyph state by clicking on letter (this is a hover view):
How it looks in devtools:
What i expect:
On download click:
I need to download this logo ("TESTING") as .svg file.
const svgHtml = Array.from(logoRef.current.querySelectorAll('span'))
.map((el) => el.innerHTML)
.join('\n');
const svg = `
<svg xmlns="http://www.w3.org/2000/svg">
${svgHtml}
</svg>
`;
const blob = new Blob([svg], { type: 'image/svg+xml' });
createDownloadLink(blob, `${inputText}.svg`);
Actual behavior:
All letters have no relative positioning as in preview:
Do you have any ideas how can i do this?
Or maybe there is a better solution you can purpose?
Thanks in advance
You need to calculate x offset values for each letter svg before merging them, since they have no idea how they are positioned in the HTML DOM context (i.e as children of <span> elements).
This offset value would be incremented by the viewBox width
(3. viewBox argument: viewBox="0 0 93 132" => width=93):
JavaScript example
let spans = document.querySelectorAll('span');
let svgMarkup = '';
let xOffset = 0;
let viewBoxHeight = 132;
let letterSpacing = 5;
spans.forEach((span,i)=>{
let svg = span.querySelector('svg');
let style = window.getComputedStyle(span)
let color = style.color;
let viewBox = svg.getAttribute('viewBox').split(' ');
let width = +viewBox[2];
let glyph = svg.querySelector('path');
// clone path to apply offset
let glyphCloned = glyph.cloneNode(true);
// apply x offset by translate()
glyphCloned.setAttribute('transform', `translate(${xOffset} 0)`);
glyphCloned.style.color=color;
let glyphMarkup = glyphCloned.outerHTML;
glyphCloned.remove();
svgMarkup+=glyphMarkup;
// increment offset for next letter
xOffset += i==spans.length-1 ? width : width+letterSpacing;
})
let svgCombined = `
<svg id="combined" xmlns="http://www.w3.org/2000/svg" width="${xOffset}" height="${viewBoxHeight}" viewBox="0 0 ${xOffset} ${viewBoxHeight}">
${svgMarkup}
</svg>`;
document.body.insertAdjacentHTML('beforeend', svgCombined);
svg{
height:10em;
width:auto;
border:1px solid #ccc;
}
.blue{
color:blue
}
<span><svg width="93" height="132" viewBox="0 0 93 132" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M75 41.76L87.6 34.92C82.2 21.6 67.2 10.8 49.08 10.8C19.92 10.8 4.8 30.12 4.8 54C4.8 77.88 19.2 97.2 49.08 97.2C67.2 97.2 82.2 86.4 87.6 73.08L75 66.12C71.04 75.72 61.92 84 48.48 84C30.72 84 19.68 71.76 19.68 54C19.68 36.24 30.72 24 48.48 24C61.92 24 71.04 32.28 75 41.76Z" fill="currentColor" />
</svg>
</span>
<span class="blue">
<svg width="96" height="132" viewBox="0 0 96 132" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.4 51.24H33.6C33.6 31.92 42.24 24 54.36 24C64.92 24 72.36 29.16 76.2 40.56L88.8 33.6C83.4 19.2 71.64 10.8 54.48 10.8C32.04 10.8 18.72 25.08 18.72 51.24H4.8C4.8 80.52 24.48 97.08 49.8 97.08C70.68 97.08 83.04 85.8 89.16 74.04L77.64 67.44C71.88 77.16 64.2 83.64 50.4 83.64C33.12 83.64 21.12 71.16 20.4 51.24Z" fill="currentColor" />
</svg>
</span>
<span><svg width="93" height="132" viewBox="0 0 93 132" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M75 41.76L87.6 34.92C82.2 21.6 67.2 10.8 49.08 10.8C19.92 10.8 4.8 30.12 4.8 54C4.8 77.88 19.2 97.2 49.08 97.2C67.2 97.2 82.2 86.4 87.6 73.08L75 66.12C71.04 75.72 61.92 84 48.48 84C30.72 84 19.68 71.76 19.68 54C19.68 36.24 30.72 24 48.48 24C61.92 24 71.04 32.28 75 41.76Z" fill="currentColor" />
</svg>
</span>
<p>Combined Svg</p>
The example above will also select only the <path> elements instead of their parent <svg>s.
In fact, you could also set an offset via x attribute.
However, a lot of graphic applications struggle with nested svgs - so copying only paths is usually a more robust solution for standalone svgs.
Besides, we're adding a viewBox based to the total width and height of all combined letter svgs.

SVG: get arc segment length

I'm looking into ways to calculate path.getTotalLength() in Node.js and it seems that a polyfill is not available. So far I managed to compute all other pathCommands except A.
For instance, knowing last X and Y from M segment, and also having all the A segment values, how to determine the length of this path?
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path d="M8 15 A7 7 0 1 0 8 1">
</svg>
Thank you
MDN has some great documentation in regards to the various path commands.
MDN > SVG Tutorial > Paths
Here is how you decompose the provided path commands:
M 8 15 = Move To (Absolute)
x = 8
y = 15
A 7 7 0 1 0 8 1 = Arc (Absolute)
rx = 7
ry = 7
x-axis-rotation = 0
large-arc-flag = 1
sweep-flag = 0
x = 8
y = 1
I followed this Mathematics Exchange post to compute the arc length, given the states path commands. Since the x and y radii of your arc are equal, this is a bit easier.
Note: I am not sure what you would need to do if they are different.
const x1 = 8, y1 = 15;
const x2 = 8, y2 = 1;
const r = 7; // Since rx === ry, this is a bit easier
const d = Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
const theta = Math.acos(1 - (Math.pow(d, 2) / (2 * Math.pow(r, 2))));
const arcLength = theta * r;
console.log(arcLength); // Arc Length = 21.9911
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path d="
M 8 15
A 7 7 0 1 0 8 1
" fill="none" stroke="black" stroke-width="2" />
<!-- Move to and begin arc at (8, 15) -->
<circle cx="8" cy="15" r="1" stroke="none" fill="red" />
<!-- End arc at (8, 1) -->
<circle cx="8" cy="1" r="1" stroke="none" fill="cyan" />
<!-- Radius of (7, 7) -->
<circle cx="15" cy="7.5" r="1" stroke="none" fill="lime" />
<!-- Center -->
<circle cx="8" cy="7.5" r="1" stroke="none" fill="gold" />
</svg>

How do I generate random positions of hexagon using svg?

How do I generate hexagons in random positions on the svg canvas?
Currently, my code uses
.attr("points", "50,25 86,45.83 86,87.5 50,108.3 14,87.53 14,45.83")
which hardcodes the current position of the hexagon. How can I generate other hexagons in different positions while maintaining the hexagonal shape?
Y would create a symbol with a viewBox attribute:
<symbol id="poly" viewBox="14 25 72 83.3">
<polygon points="50,25 86,45.83 86,87.5 50,108.3 14,87.53 14,45.83" />
</symbol>
Since the symbol has a viewBox attribute you can reuse the symbol with <use> and you can specify the position of the hexagon (x and y attributes) and it's size (width and height attributes)
svg{border:1px solid}
<svg viewBox="0 0 500 250">
<symbol id="poly" viewBox="14 25 72 83.3">
<polygon points="50,25 86,45.83 86,87.5 50,108.3 14,87.53 14,45.83" />
</symbol>
<use xlink:href="#poly" x="20" y="20" width="50" height="57.85" />
<use xlink:href="#poly" x="200" y="120" width="100" height="115.7" />
</svg>
Of course the x and y can be random. Also the width or the height can be random. However keep in mind that the other size should be proportional.
This is how I would create the use element with a random x y and width attributes:
const SVG_NS = 'http://www.w3.org/2000/svg';
const SVG_XLINK = "http://www.w3.org/1999/xlink";
//create a new use element
let use = document.createElementNS(SVG_NS, 'use');
// set the value for 'xlink:href' of the new use element
use.setAttributeNS(SVG_XLINK, 'xlink:href', '#poly');
//the random width
let w = Math.random()*50;
// the proportiopnal height
let h = w*83.3 / 75;
//set the position and the size of the use element
use.setAttributeNS(null, 'x', Math.random()*(500 - w));
use.setAttributeNS(null, 'y', Math.random()*(250 - h));
use.setAttributeNS(null, 'width', w);
use.setAttributeNS(null, 'height', h);
//Append the use element
svg.appendChild(use);
svg{border:1px solid}
<svg id="svg" viewBox="0 0 500 250">
<symbol id="poly" viewBox="14 25 72 83.3">
<polygon tran points="50,25 86,45.83 86,87.5 50,108.3 14,87.53 14,45.83" />
</symbol>
</svg>
Create a function drawHex(x,y) where you pass x and y as starting coordinates. On that function the you draw your points relative to x and y:
..."x+50,y+25 x+86,y+45.83 ... x+14,y+45.83";
Finally, create a loop that randomly generates x and y and calls the drawHex function. I'm recently working on something similar. You can take a look and my source code at this P5js experiment and then go to creaPuerta() function on https://zoada.com/lpa/js/parametrica.js
Based on Robert Longson's comment, you could do it like that:
const btn = document.getElementById('btn');
const poly = document.getElementById('poly');
btn.onclick = () => {
const transform = `translate(${getRandomArbitrary(0, 100)} ${getRandomArbitrary(0, 100)}) scale(${getRandomArbitrary(1, 5)} ${getRandomArbitrary(1, 5)})`;
poly.setAttribute('transform', transform);
};
function getRandomArbitrary(min, max) {
return Math.random() * (max - min) + min;
}
button {
display: block;
}
<button id="btn">Random</button>
<svg width=500 height=500>
<polygon id="poly" points="50,25 86,45.83 86,87.5 50,108.3 14,87.53 14,45.83"></polygon>
</svg>

Why <marker> doesn't orientate as the <path>

I'm trying to create a curved arrow with svg. I'm using d3.line() to generate the path.
let points = [
[400,100],
[450,200],
[350,200],
[385,275]
]
let path = d3.line().curve(d3.curveCardinal)(points)
console.log(path)
// -> M400,100C400,100,458.3333333333333,183.33333333333334,450,200C441.6666666666667,216.66666666666666,360.8333333333333,187.5,350,200C339.1666666666667,212.5,385,275,385,275
But when I try to use this result in a svg:
<svg width="1200" height="1200" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg" version="1.1">
<defs>
<marker id="Triangle" viewBox="0 0 10 10" refX="1" refY="5" markerWidth="6" markerHeight="6" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" />
</marker>
</defs>
<path d="M400,100C400,100,458.3333333333333,183.33333333333334,450,200C441.6666666666667,216.66666666666666,360.8333333333333,187.5,350,200C339.1666666666667,212.5,385,275,385,275"
stroke-width="2" stroke="lightblue" fill="none" style="marker-end: url(#Triangle);"></path>
</svg>
And here is the SVG result
.
I can't figure out why the marker doesn't orientate. Is there a better library to generate path to resolve this?
That's the expected behaviour. The issue is that in a cardinal spline...
Two additional points are required on either end of the curve.
And those points seem to interfere with the marker orientation (which is indeed the case, see LeBeau's answer).
You can easily see this if you change the curve. For instance, using curveBasis:
let points = [
[400,100],
[450,200],
[350,200],
[385,275]
]
let path = d3.line().curve(d3.curveBasis)(points)
d3.select("#myPath").attr("d", path);
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="1200" height="1200" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg" version="1.1">
<defs>
<marker id="Triangle" viewBox="0 0 10 10" refX="1" refY="5" markerWidth="6" markerHeight="6" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" />
</marker>
</defs>
<path id="myPath" stroke-width="2" stroke="lightblue" fill="none" style="marker-end: url(#Triangle);"></path>
</svg>
In your case, a solution (arguably a hack) may be adding a final line to the path, just 1px away from the final point:
path = path + "L387,277";
Here is the demo:
let points = [
[400,100],
[450,200],
[350,200],
[385,275]
]
let path = d3.line().curve(d3.curveCardinal)(points)
path = path + "L387,277";
d3.select("#myPath").attr("d", path);
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="1200" height="1200" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg" version="1.1">
<defs>
<marker id="Triangle" viewBox="0 0 10 10" refX="1" refY="5" markerWidth="6" markerHeight="6" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" />
</marker>
</defs>
<path id="myPath" stroke-width="2" stroke="lightblue" fill="none" style="marker-end: url(#Triangle);"></path>
</svg>
This is because the last control point and the end-point of your path have the same coordinates: (385,275).
SVG uses the control point vector to work out what the curve direction is at that point. If your control point vector is from (385,275) to (385,275), then it can't determine the angle. So it defaults to an angle of 0 degrees.
First, the ref attributes are sort of correct but can be better I think, make the refX 0 since you using the full viewBox.
I think the marker's orientation is correct and updated. But based on the ending of the path, the interpolation of the orientation might look incorrect. So you can verify this behavior by cutting your pathstring from the last C... curve and will see that the orientation is correct.
I further tested it to see if it is correct, at least for line segments, here is a fiddle and i didn't even use d3:
https://jsfiddle.net/ibowankenobi/L8x19rco/2/
var path = document.querySelector("path[stroke]");
var arr = Array.apply(null,Array(path.getTotalLength()/4 << 0)).map(function(d,i){
var p = this.getPointAtLength(i*4);
return [p.x,p.y];
},path);
var length = arr.length;
animate();
function animate(index){
if(index >= length){
return;
}
var index = index || 0;
path.setAttribute("d","M"+arr.slice(1,Math.min(++index+1,length)).join("L"));
window.requestAnimationFrame(function(){animate(index);});
}

Create svg arcs between two points

I want to connect two SVG points (e.g. the centers of two circles) using arcs. If there is only one connection, the line (<path>) will be straight. If there are two connections, both will be rounded and will be symmetrical, this way:
So, in fact, there are few rules:
Everything should be symmetrical to to the imaginary line that connects the two points.
From 1, it's obvious that if the number of connections is:
odd: we do not display the straight line
even: we display the straight line
There should be a value k which defines the distance between two connections between same points.
The tangent that goes through the middle of the elliptical arc should be parallel with the straight line that connects the two points. And obviously, the middle of the line will be perpendicular to the tangent.
I'm struggling to get a formula to calculate the A parameters in the <path> element.
What I did until now is:
<path d="M100 100, A50,20 0 1,0 300,100" stroke="black" fill="transparent"/>
M100 100 is clear: that's the starting point (move to 100,100)
Last two numbers are also clear. The path ends in 300,100
I also saw that if I put 0 instead of 20, I obtain a straight line.
If I replace 1,0 with 1,1, the path is flipped.
What I don't know is how to calculate the A parameters. I read the docs, but the imagine is still unclear to me. How to calculate these values?
svg {
width: 100%;
height: 100%;
position: absolute;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>JS Bin</title>
</head>
<body>
<?xml version="1.0" standalone="no" ?>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg">
<!-- Connect A(100,100) with B(300, 100) -->
<path d="M100 100, A50,0 0 1,0 300,100" stroke="black" fill="transparent" />
<path d="M100 100, A50,20 0 1,0 300,100" stroke="black" fill="transparent" />
<path d="M100 100, A50,20 0 1,1 300,100" stroke="black" fill="transparent" />
<path d="M100 100, A50,30 0 1,0 300,100" stroke="black" fill="transparent" />
<path d="M100 100, A50,30 0 1,1 300,100" stroke="black" fill="transparent" />
<!-- A(100, 100) B(300, 400) -->
<path d="M100 100, A50,0 57 1,0 300,400" stroke="black" fill="transparent" />
<path d="M100 100, A50,20 57 1,0 300,400" stroke="black" fill="transparent" />
<path d="M100 100, A50,20 57 1,1 300,400" stroke="black" fill="transparent" />
</svg>
</body>
</html>
I'm using SVG.js to create the paths.
You're making life very difficult for yourself by requiring circular arcs.
If you use quadratic curves instead, then the geometry becomes very simple — just offset the central X coordinate by half the difference in Y coordinates, and vice versa.
function arc_links(dwg,x1,y1,x2,y2,n,k) {
var cx = (x1+x2)/2;
var cy = (y1+y2)/2;
var dx = (x2-x1)/2;
var dy = (y2-y1)/2;
var i;
for (i=0; i<n; i++) {
if (i==(n-1)/2) {
dwg.line(x1,y1,x2,y2).stroke({width:1}).fill('none');
}
else {
dd = Math.sqrt(dx*dx+dy*dy);
ex = cx + dy/dd * k * (i-(n-1)/2);
ey = cy - dx/dd * k * (i-(n-1)/2);
dwg.path("M"+x1+" "+y1+"Q"+ex+" "+ey+" "+x2+" "+y2).stroke({width:1}).fill('none');
}
}
}
function create_svg() {
var draw = SVG('drawing').size(300, 300);
arc_links(draw,50,50,250,50,2,40);
arc_links(draw,250,50,250,250,3,40);
arc_links(draw,250,250,50,250,4,40);
arc_links(draw,50,250,50,50,5,40);
draw.circle(50).move(25,25).fill('#fff').stroke({width:1});
draw.circle(50).move(225,25).fill('#fff').stroke({width:1});
draw.circle(50).move(225,225).fill('#fff').stroke({width:1});
draw.circle(50).move(25,225).fill('#fff').stroke({width:1});
}
create_svg();
<script src="https://cdnjs.cloudflare.com/ajax/libs/svg.js/2.3.2/svg.min.js"></script>
<div id="drawing"></div>
For drawing SVG path's arc you need 2 points and radius, there are 2 points and you just need to calculate radius for given distances.
Formula for radius:
let r = (d, x) => 0.125*d*d/x + x/2;
where:
d - distance between points
x - distance between arcs
it derived from Pythagorean theorem:
a here is a half of distance between points
let r = (d, x) => !x?1e10:0.125*d*d/x + x/2;
upd();
function upd() {
let n = +count.value;
let s = +step.value/10;
let x1 = c1.getAttribute('cx'), y1 = c1.getAttribute('cy');
let x2 = c2.getAttribute('cx'), y2 = c2.getAttribute('cy');
let dx = Math.sqrt((x1-x2)*(x1-x2) + (y1-y2)*(y1-y2));
paths.innerHTML = [...Array(n)].map((_, i) => [
n%2&&i===n-1?0:1+parseInt(i/2),
i%2
]).map(i => `<path d="${[
'M', x1, y1,
'A', r(dx, s*i[0]), r(dx, s*i[0]), 0, 0, i[1], x2, y2
].join(' ')}"></path>`).join('');
}
<input id="count" type="range" min=1 max=9 value=5 oninput=upd() >
<input id="step" type="range" min=1 max=200 value=100 oninput=upd() >
<svg viewbox=0,0,300,100 stroke=red fill=none >
<circle id=c1 r=10 cx=50 cy=60></circle>
<circle id=c2 r=10 cx=250 cy=40></circle>
<g id=paths></g>
</svg>
Here is a solution that uses arcs, as asked for, rather than quadratic curves.
// Internal function
function connectInternal(x1,y1,x2,y2,con){
var dx=x2-x1
var dy=y2-y1
var dist=Math.sqrt(dx*dx+dy*dy)
if(dist==0 || con==0){
return "M"+x1+","+y1+"L"+x2+","+y2
}
var xRadius=dist*0.75
var yRadius=dist*0.3*(con*0.75)
var normdx=dx/dist
if(normdx<-1)normdx=-1
if(normdx>1)normdx=1
var angle=Math.acos(normdx)*180/Math.PI
if(x1>x2){
angle=-angle
}
return "M"+x1+","+y1+"A"+xRadius+","+yRadius+","+
angle+",00"+x2+","+y2+
"M"+x1+","+y1+"A"+xRadius+","+yRadius+","+
angle+",01"+x2+","+y2
}
// Returns an SVG path that represents
// "n" connections between two points.
function connect(x1,y1,x2,y2,n){
var ret=""
var con=n
if(con%2==1){
ret+=connectInternal(x1,y1,x2,y2,con)
con-=1
}
for(var i=2;i<=con;i+=2){
ret+=connectInternal(x1,y1,x2,y2,i)
}
return ret
}

Categories