Having an random set of paths with different dimensions. The task is to set a uniform even pattern on them. By now, it's looks like:
Needs to be
I have played with different preserveAspectRatio attribute as well as viewBox one, but without any success.
It could be done by cloning start along the screen and apply shapes as clip-paths, the task is to do everything with patterns.l
const data = {
poly0: [{"x":107,"y":392.2},{"x":186,"y":361},{"x":257,"y":355.3},{"x":257,"y":333.9},{"x":107,"y":372.4}],
poly1: [{"x":107,"y":406.5},{"x":257,"y":368},{"x":257,"y":355.3},{"x":186,"y":361},{"x":107,"y":392.2}],
poly2: [{"x":107,"y":463.8},{"x":107,"y":521},{"x":257,"y":467},{"x":360,"y":527},{"x":488,"y":486},{"x":621,"y":540},{"x":677,"y":486},{"x":677,"y":453},{"x":677,"y":420.1},{"x":621,"y":433.9},{"x":488,"y":370.9},{"x":360,"y":435.5},{"x":257,"y":368},{"x":107,"y":406.5}],
poly3: [{"x":257,"y":368},{"x":360,"y":435.5},{"x":488,"y":370.9},{"x":488,"y":338.5},{"x":360,"y":363.5},{"x":257,"y":333.9}],
poly4: [{"x":488,"y":354.5},{"x":621,"y":386.3},{"x":677,"y":388.5},{"x":677,"y":362.5},{"x":621,"y":338.5},{"x":488,"y":338.5}],
poly5: [{"x":488,"y":370.9},{"x":621,"y":433.9},{"x":677,"y":420.1},{"x":677,"y":388.5},{"x":621,"y":386.3},{"x":488,"y":354.5}]
};
let svg, g2, EPS = 1E-5, curve = d3.line().curve(d3.curveCatmullRom).x(d_ => { return d_.x; }).y(d_ => { return d_.y; });
svg = d3.select("#scene");
let colors = ["#005f73", "#0a9396", "#94d2bd", "#e9d8a6", "#ee9b00", "#ca6702", "#bb3e03", "#ae2012"];
let paths = svg.selectAll(".path")
.data(Object.keys(data))
.enter()
.append("path")
.attr("id", (d_) => { return `path_${d_}`; })
.attr("class", "path")
.attr("d", (d_) => { return generatePathFromPoints(data[d_], true); })
.attr("stroke", "#000000")
.attr("fill", "url(#star)");
function generatePathFromPoints(points_, closed_){
let d = `M${points_[0].x} ${points_[0].y}`;
for(let i = 1; i < points_.length; i++) { d += `L${points_[i].x} ${points_[i].y}`; }
if(closed_) { d += "Z"; }
return d;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg id="scene" viewBox="0 0 800 800" preserveAspectRatio="xMinYMin meet">
<defs>
<pattern id="star" viewBox="0 0 10 10" width="10%" height="10%" preserveAspectRatio="none">
<polygon points="0,0 2,5 0,10 5,8 10,10 8,5 10,0 5,2" fill="#000000"/>
</pattern>
</defs>
</svg>
Basically the behavior comes from the SVG pattern width="10%" height="10%" preserveAspectRatio="none", which instruct the pattern to be 10% the size of what it fills (paint).
While you probably needs it to be 10% the size of the whole SVG, which can be achieved by setting the patternUnits attribute to userSpaceOnUse. So it becomes:
<pattern id="star" patternUnits="userSpaceOnUse" viewBox="0 0 10 10" width="10%" height="10%" preserveAspectRatio="none">
<polygon points="0,0 2,5 0,10 5,8 10,10 8,5 10,0 5,2" fill="#000000"/>
</pattern>
Related
I am trying to mimic the behavior of "stroke alignment" in an SVG object. While there is a working draft for stroke-alignment in the spec, this has not actually been implemented (despite being drafted in 2015).
Example of non-working stroke-alignment:
The blue square should have stroke inside, the red outside, but they're both the same
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="100">
<g id="myGroup" transform="translate(20 20)">
<polygon id="myPoly0" points="0,0 50,0 50,50 0,50" style="fill:blue;stroke:black;stroke-width:4;stroke-alignment:inner"></polygon>
<polygon id="myPoly1" transform="translate(75 0)" points="0,0 50,0 50,50 0,50" style="fill:red;stroke:black;stroke-width:4;stroke-alignment:outer"></polygon>
</g>
</svg>
My approach to mimicking this behavior is to create a duplicate SVG object using the <use> element, setting a stroke property on the copy and scaling it slightly up or down depending on whether it's an inner or outer stroke alignment (default is center)
For example:
The scale and transform for the <use> element gets worse the farther from origin
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="400">
<g id="myGroup" style="fill:rgb(45, 130, 255);" transform="translate(20 20)">
<polygon id="myPoly0" points="0,0 100,0 100,100 0,100"></polygon>
<polygon id="myPoly1" transform="translate(110 110)" points="0,0 100,0 100,100 0,100"></polygon>
<polygon id="myPoly2" transform="translate(220 220)" points="0,0 100,0 100,100 0,100"></polygon>
<use id="myPolyCopy0" vector-effect="non-scaling-stroke" href="#myPoly0" style="fill:none;stroke:black;stroke-width:4;" transform="translate(-2 -2) scale(1.04 1.04)"></use>
<use id="myPolyCopy1" vector-effect="non-scaling-stroke" href="#myPoly1" style="fill:none;stroke:black;stroke-width:4;" transform="translate(-2 -2) scale(1.04 1.04)"></use>
<use id="myPolyCopy2" vector-effect="non-scaling-stroke" href="#myPoly2" style="fill:none;stroke:black;stroke-width:4;" transform="translate(-2 -2) scale(1.04 1.04)"></use>
</g>
</svg>
As you can see from the above example, the relative positioning of the <use> element goes awry, and gets worse the farther away from the origin it gets.
Naively, I assume that the transform property of the <use> element acts upon the SVG shape referenced in its href, but that seems not to be the case.
In my example, I'm scaling a 100x100 square by a factor of 1.04, which should result in a 104x104 square (4px width of the stroke). I'm then translating back by -2px to position the stroke on the outside of the source shape, thereby mimicking an outer stroke alignment.
This works as expected if the source shape is at origin (relative to the container group), but goes bonkers if the source shape is translated away from the origin.
My brain says this should work, but my browser says no bueno.
Anyone got any clues?
So, it turns out that the transform applied to the <use> element will be applied to the existing transform of the source element.
This means that the scale transform applied to the <use> element will also scale its translate matrix.
For example:
If the source element has translate(100 100), applying a scale(1.1 1.1) on the <use> copy will cause it to have a translate with the value (110,110)
This means to move the copy with the stroke value back to the correct location, you need to move the copy far enough back to overcome this "scaled translation".
This may well be expected behavior, but it was not intuitive to me (I may need to RTFM). Overall this approach "works", but feels complicated and hacky.
Working sample:
const strokeWidth = 8;
const poly1Translate = {
x: 150,
y: 20
};
const poly2Translate = {
x: 300,
y: 40
};
const poly1 = document.getElementById("myPoly1");
const poly2 = document.getElementById("myPoly2");
const polyCopy0 = document.getElementById("myPolyCopy0");
const polyCopy1 = document.getElementById("myPolyCopy1");
const polyCopy2 = document.getElementById("myPolyCopy2");
const styleString = `fill:none;stroke:red;stroke-opacity:0.5;stroke-width:${strokeWidth};`;
poly1.setAttribute(
"transform",
`translate(${poly1Translate.x} ${poly1Translate.y})`
);
poly2.setAttribute(
"transform",
`translate(${poly2Translate.x} ${poly2Translate.y})`
);
polyCopy0.setAttribute("style", styleString);
polyCopy1.setAttribute("style", styleString);
polyCopy2.setAttribute("style", styleString);
// Use the boundingbox to get the dimensions
const poly1BBox = poly1.getBBox();
const poly2BBox = poly2.getBBox();
let halfStrokeWidth = strokeWidth / 2;
// stroke-alignment:outside
// Scale the copy to be strokeWidth pixels larger
let scaleOutsideX = 1+strokeWidth/poly1BBox.width;
let scaleOutsideY = 1+strokeWidth/poly1BBox.height;
// Move the copy to the same scale property based on the current translation
// This will position the stroke at the correct origin point, and we need to
// deduct a further half of the stroke width to position it fully on the outside
let translateOutsideX = -((poly1Translate.x * scaleOutsideX - poly1Translate.x) + halfStrokeWidth);
let translateOutsideY = -((poly1Translate.y * scaleOutsideY - poly1Translate.y) + halfStrokeWidth);
polyCopy1.setAttribute('transform', `translate(${translateOutsideX} ${translateOutsideY}) scale(${scaleOutsideX} ${scaleOutsideY})`);
// stroke-alignment:inside
let scaleInsideX = 1-strokeWidth/poly2BBox.width;
let scaleInsideY = 1-strokeWidth/poly2BBox.height;
let translateInsideX = poly2Translate.x * scaleOutsideX - poly2Translate.x + halfStrokeWidth;
let translateInsideY = poly2Translate.y * scaleOutsideY - poly2Translate.y + halfStrokeWidth;
polyCopy2.setAttribute('transform', `translate(${translateInsideX} ${translateInsideY}) scale(${scaleInsideX} ${scaleInsideY})`);
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="160">
<g id="myGroup" style="fill:rgb(45, 130, 255);" transform="translate(20 20)">
<polygon id="myPoly0" points="0,0 100,0 100,100 0,100"></polygon>
<polygon id="myPoly1" points="0,0 100,0 100,100 0,100"></polygon>
<polygon id="myPoly2" points="0,0 100,0 100,100 0,100"></polygon>
<use id="myPolyCopy0" vector-effect="non-scaling-stroke" href="#myPoly0"></use>
<use id="myPolyCopy1" vector-effect="non-scaling-stroke" href="#myPoly1"></use>
<use id="myPolyCopy2" vector-effect="non-scaling-stroke" href="#myPoly2"></use>
</g>
</svg>
UPDATE
After noticing the following comment in the Figma website:
Inside and outside stroke are actually implemented by doubling the stroke weight and masking the stroke by the fill. This means inside-aligned stroke will never draw strokes outside the fill and outside-aligned stroke will never draw strokes inside the fill.
I implemented a similar method using a combination of <clipPath> and <mask>
.stroke {
fill:none;
stroke:red;
stroke-opacity:0.5;
}
.stroke-center {
stroke-width:8;
}
.stroke-inout {
stroke-width:16;
}
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="160">
<defs>
<rect id="stroke-mask" width="500" height="160" fill="white"/>
</defs>
<g id="myGroup" style="fill:rgb(45, 130, 255);" transform="translate(20,20)">
<polygon id="myPoly0" points="0,0 100,0 100,100 0,100" transform="translate(0,20)"></polygon>
<polygon id="myPoly1" points="0,0 100,0 100,100 0,100" transform="translate(150,20)"></polygon>
<polygon id="myPoly2" points="0,0 100,0 100,100 0,100" transform="translate(300,20)"></polygon>
<mask id="mask">
<use href="#stroke-mask"/>
<use href="#myPoly1" fill="black"/>
</mask>
<clipPath id="clip">
<use href="#myPoly2"/>
</clipPath>
<use id="myPolyCopy0" class="stroke stroke-center" href="#myPoly0"></use>
<use id="myPolyCopy1" class="stroke stroke-inout" href="#myPoly1" mask="url(#mask)"></use>
<use id="myPolyCopy2" class="stroke stroke-inout" href="#myPoly2" clip-path="url(#clip)"></use>
</g>
</svg>
The idea here is, to achieve the equivalent of:
stroke-align:center: is the default behavior, do nothing
stroke-align:inner: Create a clipPath using the source object to which you want to apply the inner stroke, then with the <use> element, create a copy of this with a stroke twice the width you actually want, and set the clip-path of the copy to be the clipPath created from the source object. This will effectively clip everything outside the clipPath, thereby clipping the "outside half" of the double-width stroke
stroke-align:outer: There isn't an equivalent of clipPath which will clip everything inside the path (sadly), so the way to achieve this is to use a <mask>, but the same principle applies as for inner. Create a <mask> based on the source object, create a copy with a double-width stroke, then use the mask to clip everything inside the mask, thereby clipping the "inside half" of the double-width stroke
This question already has an answer here:
Declaratively stroke a line with a composite line symbol using SVG
(1 answer)
Closed 7 months ago.
Is there a better solution to add to a path element a dashed border (lets say the border should have an offset of 2px in each direction)
I am looking for a general solution for a lot of path elements
For example my initial path element would be
<path stroke="black" fill="none" d="M10 10 L 50 10 L 50 80 L 10 80 Z"></path>
and at the moment I am creating another path element to add the border around the initial path element
<path stroke="black" stroke-dasharray="3" fill="none" d="M6 6 L 54 6 L 54 84 L 6 84 Z"></path>
<svg height="1000" width="1000">
<path stroke="black" fill="none"
d="M10 10 L 50 10 L 50 80 L 10 80 Z"></path>
<path stroke="black" stroke-dasharray="3"
fill="none" d="M6 6 L 54 6 L 54 84 L 6 84 Z"></path>
</svg>
"a general solution for a lot of path elements" is pretty vague. I'll handle simple closed paths in this answer.
This way may actually be more of an example how not to do it, but I think it shows a general problem with what you try to achieve. It uses only one place to define a path, and then re-uses it in three other places:
first, to draw the inner border,
then, to draw a much wider dashed border,
and finally, as a mask to hide that part of the dashed border that would otherwise overlap the inner one.
This has the advantage of not having to create extra paths, but the dashed border looks strange. The corners either show gaps or exta-long dashes, and in curved sections the length of the dashes are differing.
.distance {
stroke-width: 6;
}
.inner {
fill:none;
stroke: black;
stroke-width: 2;
}
.outer {
stroke: black;
stroke-width: 10;
stroke-dasharray: 4;
stroke-dashoffset: 2;
}
<svg viewBox="0 0 100 100" width="200" height="200">
<defs>
<path id="src" d="M 32,13 20,42 Q 30,90 85,90 L 92,53 Q 60,53 60,13 Z" />
<mask id="mask">
<rect width="100%" height="100%" fill="white" />
<use class="distance" href="#src" stroke="black" />
</mask>
</defs>
<use class="inner" href="#src" />
<use class="outer" href="#src" mask="url(#mask)" />
</svg>
Why is that so? The dashes are computed in relation to where the original path is, but what is shown is only the outer fringe of the whole stroke, at an offset. (or to put it the other way round, the path defining where dashes start and end is at an offset from the middle of the shown dashed line.) For concave sections, dashes get longer, and for convex sections, shorter.
The only way the dash length can be stable is when the path used to compute dashes sits in the middle of the dashes. You could change the order around and define the dashes on the inner border:
.distance {
stroke-width: 6;
}
.inner {
fill:none;
stroke: black;
stroke-width: 2;
stroke-dasharray: 4;
}
.outer {
stroke: black;
stroke-width: 10;
}
<svg viewBox="0 0 100 100" width="200" height="200">
<defs>
<path id="src" d="M 32,13 20,42 Q 30,90 85,90 L 92,53 Q 60,53 60,13 Z" />
<mask id="mask">
<rect width="100%" height="100%" fill="white" />
<use class="distance" href="#src" stroke="black" />
</mask>
</defs>
<use class="inner" href="#src" />
<use class="outer" href="#src" mask="url(#mask)" />
</svg>
..but that is as far as you get. The bottom line remains: you need to have a path where the dashed line is, not at an offset.
Use a native JavaScript Web Component <svg-outline> (you define once)
to do the work on an <svg>
<svg-outline>
<svg viewBox="0 0 100 100" height="180">
<path outline="blue" fill="none" d="M5 5 L 50 30 L 50 40 L 10 80 Z"/>
</svg>
</svg-outline>
The Web Component clones your original shapes (marked with "outline" attribute)
sets a stroke and stroke-dasharray on it
removes any existing fill
transforms clone
translates clone to account for scale(1.2)
scales clone to 1.2 size
then corrects translate
SO snippet output:
See JSFiddle: https://jsfiddle.net/WebComponents/2goahcqv/
<svg-outline>
<style> circle[outline] { stroke: blue } </style>
<svg viewBox="0 0 100 100" height="180">
<rect outline="green" x="15" y="15" width="50%" height="50%" stroke="blue" fill="teal"/>
<circle outline fill="lightcoral" cx="50" cy="50" r="10"/>
</svg>
</svg-outline>
<svg-outline>
<svg viewBox="0 0 100 100" height="180">
<path outline="blue" fill="pink" d="M15 10 L 50 30 L 50 40 L 20 70 Z"/>
</svg>
</svg-outline>
<script>
customElements.define("svg-outline", class extends HTMLElement {
connectedCallback() {
setTimeout(() => { // make sure innerHTML is parsed
let svg = this.querySelector("svg");
svg.querySelectorAll('[outline]').forEach(original => {
let outlined = svg.appendChild(original.cloneNode(true));
original.after(outlined); // so we don't create "z-index" issues
let outline_stroke = outlined.getAttribute("outline") || false;
if (outline_stroke) outlined.setAttribute("stroke", outline_stroke );
original.removeAttribute("outline"); // so we can use CSS on outlines
let {x,y,width,height} = original.getBBox();
let cx = x + width/2;
let cy = y + height/2;
outlined.setAttribute("fill", "none"); // outlines never filled
outlined.setAttribute("stroke-dasharray", 3); // or read from your own attribute, like "outline"
outlined.setAttribute("transform", `translate(${cx} ${cy}) scale(${1.2}) translate(-${cx} -${cy})`);
});
// (optional) whack everything into shadowDOM so styles don't conflict
this.attachShadow({mode:"open"}).append(...this.children);
})
}
})
</script>
Given an SVG, is there a library which helps me to turn any path of the file into a sequence of points which lie on the path, with the length of path between two consecutive points being 1 (or some other constant)?
There is no need for a library. The SVGGeometryElement API makes this possible within a few lines of code:
function getPointsAlongPath (path, distance) {
const length = path.getTotalLength();
const points = [];
for (let step = 0; step <= length; step = step + distance) {
points.push(path.getPointAtLength(step));
}
return points;
}
const result = getPointsAlongPath(document.querySelector('circle'), 5);
console.log(result.map(p => [p.x, p.y]));
<svg width="100" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="40" fill="none" stroke="black" />
</svg>
I want to have a curved text along a path (half circle) in SVG. I have followed this tutorial, which is great: https://css-tricks.com/snippets/svg/curved-text-along-path/
The problem is that the path presented there works only for this specific text - Dangerous Curves Ahead. If you leave only Dangerous word that's what happens: https://codepen.io/anon/pen/pqqVGa - it no longer works (the text is no more evenly spreaded across the path).
I want to have it work regardless of text length. How to achieve that?
Using the attributes lengthAdjust and textLength you can adjust the length of the text and the height of the letters, thereby placing the text of the desired length on a segment of a fixed length
<svg id="svg1" version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" width="500" height="300" viewBox="0 0 500 300">
<path id="path1" fill="none" stroke="black" d="M30,151 Q215,21 443,152 " />
<text id="txt1" lengthAdjust="spacingAndGlyphs" textLength="400" font-size="24">
<textPath id="result" method="align" spacing="auto" startOffset="1%" xlink:href="#path1">
<tspan dy="-10"> very long text very long text very long text </tspan>
</textPath>
</text>
</svg>
Using the attribute startOffset =" 10% " you can adjust the position of the first character of the phrase
<svg id="svg1" version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" width="500" height="300" viewBox="0 0 500 300" >
<path id="path1" fill="none" stroke="black" d="M30,151 Q215,21 443,152 " />
<text id="txt1" lengthAdjust="spacingAndGlyphs" textLength="400" font-size="24">
<textPath id="result" method="align" spacing="auto" startOffset="15%" xlink:href="#path1">
<tspan dy="-10"> very long text very long text very long text </tspan>
</textPath>
</text>
</svg>
and make animation using this attribute (click canvas)
<svg id="svg1" version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" width="500" height="300" viewBox="0 0 500 300">
<path id="path1" fill="none" stroke="black" d="M30,151 Q215,21 443,152 " />
<text id="txt1" lengthAdjust="spacingAndGlyphs" textLength="200" font-size="24">
<textPath id="result" method="align" spacing="auto" startOffset="-100%" xlink:href="#path1">
<tspan dy="-10"> Very long text Very long text Very long text </tspan>
<animate
begin="svg1.click"
dur="15s"
attributeName="startOffset"
values="-100%;1%;1%;100%;1%;1%;-100%"
repeatCount="5"/>
</textPath>
</text>
<text x="200" y="150" font-size="24" fill="orange" >Click me </text>
</svg>
This is assuming that the initial text size (35) is too small.
let curveLength = curve.getTotalLength();
let fs = 35;//the initial font size
test.setAttributeNS(null, "style", `font-size:${fs}px`)
while(test.getComputedTextLength() < curveLength){
fs++
test.setAttributeNS(null, "style", `font-size:${fs}px`)
}
body {
background-color: #333;
}
text {
fill: #FF9800;
}
<svg viewBox="0 0 500 500">
<path id="curve" d="M73.2,148.6c4-6.1,65.5-96.8,178.6-95.6c111.3,1.2,170.8,90.3,175.1,97" />
<text id="test">
<textPath xlink:href="#curve">
Dangerous
</textPath>
</text>
</svg>
UPDATE
The OP is commenting:
Thanks for the response. Instead of adjusting the font size, I would prefer to create a new path that is longer / smaller and matches the text width. Not sure how to do this tho. – feerlay
Please read the comments in the code. In base of the length of the text I'm calculating the new path, but I'm assuming a lot of things: I'm assuming the new path starts in the same point as the old one.
let textLength = test.getComputedTextLength();
// the center of the black circle
let c = {x:250,y:266}
// radius of the black circle
let r = 211;
// the black arc starts at point p1
let p1 = {x:73.2,y:150}
// the black arc ends at point p2
let p2 = {x:426.8,y:150}
// distance between p1 and p2
let d = dist(p1, p2);
// the angle of the are begining at p1 and ending at p2
let angle = Math.asin(.5*d/r);
// the radius of the new circle
let newR = textLength / angle;
// the distance between p1 and the new p2
let newD = 2 * Math.sin(angle/2) * newR;
// the new attribute c for the path #curve
let D = `M${p1.x},${p1.y} A`
D += `${newR}, ${newR} 0 0 1 ${p1.x + newD},${p1.y} `
document.querySelector("#curve").setAttributeNS(null,"d",D);
// a function to calculate the distance between two points
function dist(p1, p2) {
let dx = p2.x - p1.x;
let dy = p2.y - p1.y;
return Math.sqrt(dx * dx + dy * dy);
}
body {
background-color: #333;
}
text {
fill: #FF9800;
};
<svg viewBox="0 0 500 500">
<path id="black_circle" d="M73.2,148.6c4-6.1,65.5-96.8,178.6-95.6c111.3,1.2,170.8,90.3,175.1,97" />
<path id ="curve" d="M73.2,150 A 211,211 0 0 1 426.8,150" fill="#777" />
<text id="test">
<textPath xlink:href="#curve">
Dangerous curves
</textPath>
</text>
</svg>
I need to draw nice stroked block arrow using SVG from one point (x0,y0) to another (x1,y1), like the one on the picture.
The only way I can imagine is to use a line (two lines basically to simulate stroke and fill) with marker, but it looks kind of ugly due to overlaping strokes.
Ideally both line and marker should be filled with the same color and should have the same stroke color, and overall arrow width can be fixed (but if I could parametrize that as well it would be cool). Basically it should look the same as on picture provided and should be able to be drawn by just providing coordinates of two points.
Is it even possible?
I was bored, so here you go. I have written a function to generate a path of the right shape.
You just need to give it the "from" and "to" coords, the line width, arrowhead width, and arrowhead length.
Enjoy!
var from = {x: 50, y: 250};
var to = {x: 250, y: 100};
var lineWidth = 30;
var arrowheadWidth = 60;
var arrowheadLength = 50;
var svg = document.getElementById("test");
drawArrow(svg, from, to, lineWidth, arrowheadWidth, arrowheadLength);
function drawArrow(svg, from, to, lineWidth, arrowheadWidth, arrowheadLength)
{
var dx = to.x - from.x;
var dy = to.y - from.y;
// Calculate the length of the line
var len = Math.sqrt(dx * dx + dy * dy);
if (len < arrowheadLength) return;
// The difference between the line width and the arrow width
var dW = arrowheadWidth - lineWidth;
// The angle of the line
var angle = Math.atan2(dy, dx) * 180 / Math.PI;
// Generate a path describing the arrow. For simplicity we define it as a
// horizontal line of the right length, and starting at 0,0. Then we rotate
// and move it into place with a transform attribute.
var d = ['M', 0, -lineWidth/2,
'h', len - arrowheadLength,
'v', -dW / 2,
'L', len, 0,
'L', len - arrowheadLength, arrowheadWidth / 2,
'v', -dW / 2,
'H', 0,
'Z' ];
var path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("d", d.join(' '));
path.setAttribute("transform", "translate("+from.x+","+from.y+") rotate("+angle+")");
path.setAttribute("class", "arrow-line");
svg.appendChild(path);
}
.arrow-line {
fill: gold;
stroke: black;
stroke-width: 6;
}
<svg id="test" width="300" height="300">
</svg>
the easiest way to do this is to just use script to create an arrow.
Here i simply determine the length and the angle of the arrow from the two points p1 and p2, and then create a simple path in the correct length and rotate it by the calculated angle:
svgns="http://www.w3.org/2000/svg"
function arrow(p1,p2){
var h1=15 // line thickness
var h2=35 // arrow height
var w2=22 // arrow width
var deg = Math.atan2(p1.y - p2.y, p1.x - p2.x) * (180 / Math.PI);
var len = Math.sqrt(Math.pow(p1.y - p2.y,2)+Math.pow(p1.x - p2.x,2))
var arr = document.createElementNS(svgns,"path")
var d = `M${p1.x} ${p1.y-h1/2}v${h1}h${h2/2-len}v${(h2-h1)/2}l${-w2} ${-h2/2}l${w2} ${-h2/2}v${(h2-h1)/2}z`
arr.setAttribute("d",d)
arr.setAttribute("transform",`rotate(${deg} ${p1.x} ${p1.y})`)
arr.classList.add("arrow")
return arr
}
var a1 = arrow({x:50,y:50},{x:200,y:200})
var a2 = arrow({x:450,y:50},{x:300,y:200})
var a3 = arrow({x:450,y:450},{x:300,y:300})
var a4 = arrow({x:50,y:450},{x:200,y:300})
svg.appendChild(a1)
svg.appendChild(a2)
svg.appendChild(a3)
svg.appendChild(a4)
.arrow{stroke-width:3px}
.arrow:nth-of-type(1){fill:green;stroke:lime}
.arrow:nth-of-type(2){fill:red;stroke:orange}
.arrow:nth-of-type(3){fill:blue;stroke:turquoise}
.arrow:nth-of-type(4){fill:violet;stroke:pink}
<svg id="svg" viewBox="0 0 500 500" width="400" height="400">
</svg>
if you try to be fancy and find a scriptless solution, there will be a lot of loop you have to hop...
you will need at least 4 arrows each pointing from top left to bottom right, from top right to bottom left, from bottom left to top right and from bottom right to top left...
here is a prove of concept that it is doable, but i strongly advice against it...
svg{overflow:visible;}
<svg width="200" height="200" style="overflow:visible" stroke="red" color="orange" opacity="0.5">
<marker id="ah" viewBox="0 0 10 10" orient="auto" refX="10" refY="5" overflow="visible">
<path d="M0 0L10 5L0 10z" stroke-width="1"/>
</marker>
<marker id="ah2" viewBox="0 0 10 10" orient="auto" refX="10" refY="5">
<path d="M0 0L10 5L0 10z" fill="currentColor" stroke="none"/>
</marker>
<marker id="block" viewBox="0 0 10 10" orient="auto" refX="9" refY="5">
<rect x="0" y="0" width="10" height="10" stroke="white" stroke-width="1"/>
</marker>
<marker id="block2" viewBox="0 0 10 10" orient="auto" refX="9" refY="5">
<rect x="0" y="0" width="10" height="10" stroke-width="5"/>
</marker>
<mask id="m1">
<rect x="-10%" y="-10%" width="110%" height="110%" fill="white"/>
<line x1="99.999%" y1="99.999%" x2="100%" y2="100%" stroke-width="20" marker-end="url(#block)"/>
</mask>
<line x1="0.001%" y1="0.001%" x2="0%" y2="0%" stroke-width="8" marker-end="url(#block2)"/>
<line x1="0" y1="0" x2="100%" y2="100%" stroke-width="25" mask="url(#m1)"/>
<line x1="99.999%" y1="99.999%" x2="100%" y2="100%" stroke-width="20" marker-end="url(#ah)"/>
<line x1="0" y1="0" x2="100%" y2="100%" stroke-width="20" stroke="currentColor" mask="url(#m1)"/>
<line x1="99.999%" y1="99.999%" x2="100%" y2="100%" stroke-width="20" marker-end="url(#ah2)"/>
</svg>
After sitting a few hours of triple checking all my math:
Created an normalized arrow in the SVG defs tag
Then scaling the arrow after the provided coordinates.
(Added a static height XD)
document.addEventListener("DOMContentLoaded", function(event) {
var svgDoc = document.getElementById("arrowSvg");
var useArrow = svgDoc.getElementById("customArrow");
var extraData = useArrow.getAttribute("extra:data");
extraData = extraData.split(" ");
var x1 = parseInt(extraData[0]);
var x2 = parseInt(extraData[1]);
var y1 = parseInt(extraData[2]);
var y2 = parseInt(extraData[3]);
var arrowHeight = 15;
//Calculate the rotation needed
var deltaY = y1 - y2;
var deltaX = x2 - x1;
var angle = Math.atan2(deltaY, deltaX) * (180 / Math.PI);
//Distance between the two points.
var distance = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
useArrow.setAttribute("transform",
"translate("+(x1+(deltaX/2))+" "+(y1-(deltaY/2))+") "+
"rotate(" + -1*angle +") " +
"matrix("+distance+", 0, 0, "+arrowHeight+", "+(0.5-distance*0.5)+","+(0.5-arrowHeight* 0.5)+")");
});
svg {
width: 50%;
border: 1px solid black;
}
.arrow {
stroke: black;
stroke-width: 0.05;
fill: yellow;
}
<svg id="arrowSvg" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:extra="ExtraNameSpace">>
<defs>
<path id="idArrow" class="arrow" d="M0,0.25 0.60,0.25
0.60,0 1,0.5 0.60,1
0.60,0.75 0,0.75z" />
</defs>
<!--- Extra Data Param: x1 x2 y1 y2--->
<use id="customArrow" xlink:href="#idArrow" extra:data="10 90 90 5" />
</svg>