SVG: arrow head marker with rounded joins/caps - svg

I am working on a setup, where it is possible for a user to draw an arrow at a certain canvas_size on top of e.g. a picture with a fixed aspect ratio. If watching the picture on a different screen_size the arrow should still have the same thickness, and still be pointing at the same thing (if e.g. resizing the screen the arrow_marker_size is recomputed (not included in code-snippet)).
Currently I have set this up by having a transparent line with an arrow head marker with markerUnits="strokeWidth" and then I have a line with the wanted color with vectorEffect="non-scaling-stroke" (I have been able to create a more simple solution with a fixed arrow_marker_size and a single line, but that doesn't work in Safari, where the arrow head e.g. gets very small when lowering the screen_size). This seems to work, but I am open for better solutions - but my actual problem is that I can't figure out how to make sure that the arrow head has rounded corners???
class SvgWithArrow extends React.Component {
constructor(props) {
super(props);
this.state = {
arrow_marker_size: 5
};
}
render = () => {
const { x1, y1, x2, y2, color } = this.props.arrow;
const canvasPositionStyle = {
width: this.props.screen_size.width + 'px',
height: this.props.screen_size.height + 'px',
background: "blue"
};
return (
<div style={canvasPositionStyle}>
<svg viewBox={'0 0 ' + this.props.canvas_size.width + ' ' + this.props.canvas_size.height}>
<line
x1={x1}
y1={y1}
x2={x2}
y2={y2}
markerEnd="url(#arrow-1)"
stroke="transparent"
strokeWidth="3px"
/>
<marker
id="arrow-1"
markerWidth={this.state.arrow_marker_size}
markerHeight={this.state.arrow_marker_size}
refX={this.state.arrow_marker_size}
refY={this.state.arrow_marker_size / 2}
orient="auto"
markerUnits="strokeWidth"
>
<path
fill="none"
stroke={color}
vectorEffect="non-scaling-stroke"
// TODO rounded corners?!! This isn't working...
strokeLinejoin="round"
strokeLinecap="round"
d={'M0, ' + this.state.arrow_marker_size
+ ' L' + this.state.arrow_marker_size + ','
+ (this.state.arrow_marker_size / 2) + ' 0, 0'}
/>
</marker>
<line
x1={x1}
y1={y1}
x2={x2}
y2={y2}
stroke={color}
strokeWidth="3px"
strokeLinecap="round"
vectorEffect="non-scaling-stroke"
/>
</svg>
</div>
);
}
}
// Render it
ReactDOM.render(
<SvgWithArrow
screen_size={{ width: 100*(16/9), height: 100 }}
canvas_size={{ width: 150*(16/9), height: 150 }}
arrow={{x1: 100, y1: 110, x2: 50, y2: 40, color: "red" }} />,
document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

The path line of the marker is cut off, so the marker "box" needs to be larger than the line ā€“ or the line needs to be smaller. So, the linecaps are there, but they are outside the marker element. I changed the path so that it has that margin stating with ā€“ something like: d="M 1 5 L 5 3 L 1 1" and adding 1 to this.state.arrow_marker_size some places.
class SvgWithArrow extends React.Component {
constructor(props) {
super(props);
this.state = {
arrow_marker_size: 5
};
}
render = () => {
const { x1, y1, x2, y2, color } = this.props.arrow;
const canvasPositionStyle = {
width: this.props.screen_size.width + 'px',
height: this.props.screen_size.height + 'px',
background: "blue"
};
return (
<div style={canvasPositionStyle}>
<svg viewBox={'0 0 ' + this.props.canvas_size.width + ' ' + this.props.canvas_size.height}>
<line
x1={x1}
y1={y1}
x2={x2}
y2={y2}
markerEnd="url(#arrow-1)"
stroke="transparent"
strokeWidth="3"
/>
<marker
id="arrow-1"
markerWidth={this.state.arrow_marker_size+1}
markerHeight={this.state.arrow_marker_size+1}
refX={this.state.arrow_marker_size}
refY={(this.state.arrow_marker_size+1) / 2}
orient="auto"
markerUnits="strokeWidth"
>
<path
fill="none"
stroke={color}
vectorEffect="non-scaling-stroke"
// TODO rounded corners?!! This isn't working...
strokeLinejoin="round"
strokeLinecap="round"
d={`M 1 ${this.state.arrow_marker_size}
L ${this.state.arrow_marker_size} ${((this.state.arrow_marker_size+1) / 2)}
L 1 1`}
/>
</marker>
<line
x1={x1}
y1={y1}
x2={x2}
y2={y2}
stroke={color}
strokeWidth="3px"
strokeLinecap="round"
vectorEffect="non-scaling-stroke"
/>
</svg>
</div>
);
}
}
// Render it
ReactDOM.render(
<SvgWithArrow
screen_size={{ width: 100*(16/9), height: 100 }}
canvas_size={{ width: 150*(16/9), height: 150 }}
arrow={{x1: 100, y1: 110, x2: 50, y2: 40, color: "red" }} />,
document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

Related

SVG - Draw scrolling issue with multiple instances

I am trying to create a page (in Wordpress), which is essentially a timeline. As the user scrolls to the next sections, I have a vertical line that "connects" to the next content section. After a LOT of trial and error, I was able to create a line that "draws" itself on scroll, and reverses when scrolling back up. My issue is, when I try to use the code again in the same page, it is already drawn, -- in other words, I *think there is an issue with the code not knowing that is is not supposed to trigger yet. I do not know enough about this to know why it is not working. ideally, I want each line to start drawing as the view-box/browser window is in view.
I have tried creating unique ID's, unique div's and ID's, etc. I originally thought it may be an issue with needing unique containers/ID's. Now, I am *thinking it might be because I do not know how to tell the "line" to not be visible until it is pulled into view.
Here is my pen:
// Get the id of the <path> element and the length of <path>
var triangle = document.getElementById("triangle");
var length = triangle.getTotalLength();
// The start position of the drawing
triangle.style.strokeDasharray = length;
// Hide the triangle by offsetting dash. Remove this line to show the triangle before scroll draw
triangle.style.strokeDashoffset = length;
// Find scroll percentage on scroll (using cross-browser properties), and offset dash same amount as percentage scrolled
window.addEventListener("scroll", myFunction);
function myFunction() {
var scrollpercent = (document.body.scrollTop + document.documentElement.scrollTop) / (document.documentElement.scrollHeight - document.documentElement.clientHeight);
var draw = length * scrollpercent;
// Reverse the drawing (when scrolling upwards)
triangle.style.strokeDashoffset = length - draw;
}
body {
height: 200vh;
}
#mySVG {
position: relative;
top: 15%;
left: 50%;
width: 50px;
height: 710px;
}
<svg id="mySVG" preserveAspectRatio="none" viewBox="0 0 4 100">
<path fill="none" stroke="#000000" stroke-width="1"
id="triangle" d="M 0 0 V 100 0"/>
</svg>
Whenever you need to manipulate multiple elements, you need to query these elements to get an array/list and then loop through all nodes in this array.
Usually it's a better approach to use class names to avoid non-unique IDs like so
let triangles = document.querySelectorAll(".triangle");
triangles.forEach( (triangle) => {
// do something
});
So add class names (or just replace id attributes) to your <path> elements and add a scroll EventListener
let triangles = document.querySelectorAll(".triangle");
// calculate path length and init
triangles.forEach((triangle) => {
let pathLength = triangle.getTotalLength();
triangle.setAttribute("stroke-dasharray", `0 ${pathLength}`);
});
// Find scroll percentage on scroll
window.addEventListener("scroll", (e) => {
drawLines(triangles);
});
function drawLines(triangle, pathLength) {
var scrollpercent =
(document.body.scrollTop + document.documentElement.scrollTop) /
(document.documentElement.scrollHeight -
document.documentElement.clientHeight);
triangles.forEach((triangle) => {
let pathLength = triangle.getAttribute("stroke-dasharray").split(" ")[1];
var dashLength = pathLength * scrollpercent;
triangle.setAttribute("stroke-dasharray", `${dashLength} ${pathLength}`);
});
}
body {
padding:0 5em;
height: 200vh;
margin:0;
}
svg {
position: relative;
top: 15%;
left: 50%;
width: 50px;
height: 710px;
}
path{
transition:0.4s 0.4s;
}
<svg preserveAspectRatio="none" viewBox="0 0 4 100">
<path fill="none" stroke="#000000" stroke-width="1" class="triangle" d="M 0 0 V 100 0" />
</svg>
<svg preserveAspectRatio="none" viewBox="0 0 4 100">
<path fill="none" stroke="#000000" stroke-width="1" class="triangle" d="M 0 0 V 100 0"/>
</svg>
You can also simplify your stroke animation by using the stroke-dasharray attributes specifying 2 arguments:
dashlength
dash gap
stroke-dasharray="50 100"
You can even skip the length calculation getTotalLength() by applying these fixed initial attributes pathLength="100" and stroke-dasharray="0 100".
This way you can work with percentages without the need to calculate the exact length of your element.
let triangles = document.querySelectorAll(".triangle");
// Find scroll percentage on scroll
window.addEventListener("scroll", (e) => {
drawLines(triangles);
});
function drawLines(triangle, pathLength) {
var scrollpercent =
(document.body.scrollTop + document.documentElement.scrollTop) /
(document.documentElement.scrollHeight -
document.documentElement.clientHeight);
triangles.forEach((triangle) => {
var dashLength = 100 * scrollpercent;
triangle.setAttribute("stroke-dasharray", `${dashLength} 100`);
});
}
body {
padding:0 5em;
height: 200vh;
margin:0;
}
svg {
position: relative;
top: 15%;
left: 50%;
width: 50px;
height: 710px;
}
path{
transition:0.4s 0.4s;
}
<svg preserveAspectRatio="none" viewBox="0 0 4 100">
<path fill="none" stroke="#000000" stroke-width="1" class="triangle" d="M 0 0 V 100 0" pathLength="100" stroke-dasharray="0 100"/>
</svg>
<svg preserveAspectRatio="none" viewBox="0 0 4 100">
<path fill="none" stroke="#000000" stroke-width="1" class="triangle" d="M 0 0 V 100 0" pathLength="100" stroke-dasharray="0 100"/>
</svg>

SVG elements to zoom whole SVG group on click or mouseover

I would like to use the circles within my SVG file to trigger a zoom in centred on the circle. I have got it working with a div acting as the trigger for the zoom but if I instead apply id="pin" to one of the circle elements within the SVG it no longer zooms in. Can anyone tell me why this is?
Is there a better way for me to achieve what I am trying to do? Ideally, I would like it to be possible to click to zoom and then to access other interactivity within the SVG while zoomed in.
If this is not possible is there a simple way to zoom and pan an SVG and to be able to access SVG interactivity while zoomed?
If I have missed something obvious please forgive me, Iā€™m very much still learning the basics!
Rough example:
CodePen link
<div id="pin">click to trigger zoom</div>
<div class="map" id="mapFrame">
<svg class="image" id="mapSVG" version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1920 1442.5"" style="
enable-background:new 0 0 1920 924.9;" xml:space="preserve">
<g id="Layer_7" data-name="Layer 7">
<image width="1800" height="1350" transform="translate(0) scale(1.069)" opacity="0.3"
xlink:href="https://media.npr.org/assets/img/2020/07/04/seamus-coronavirus-d3-world-map-20200323_wide-a3888a851b91a905e9ad054ea03e177e23620015.png" />
</g>
<g id="one">
<circle cx="929.664" cy="944.287" r="81.191"/>
</g>
<g id="two">
<circle cx="638.164" cy="456.863" r="81.191" />
</g>
<g id="three">
<circle cx="1266.164" cy="498.868" r="81.191" />
</g>
</svg>
</div>
<script src="app.js"></script>
svg {
width: 100%;
height: auto;
}
#pin {
position: absolute;
height: 65px;
width: 75px;
top: 300px;
left: 550px;
padding: 10px;
background-color: yellow;
}
let imgElement = document.querySelector('#mapFrame');
let pinElement = document.querySelector('#pin');
pinElement.addEventListener('click', function (e) {
imgElement.style.transform = 'translate(-' + 0 + 'px,-' + 0 + 'px) scale(2)';
pinElement.style.display = 'none';
});
imgElement.addEventListener('click', function (e) {
imgElement.style.transform = null;
pinElement.style.display = 'block';
});
When you click on the circle, you are also clicking on the background image as well, triggering two events which is essentially cancelling the zoom. You can see this if you place alert('click 1'); and alert('click 2'); in your listeners.
This doesn't happen on the #pin element because it's outside background div and avoids the event bubbling up. This is solved by adding event.stopPropagation();
Code from your CodePen:
let imgElement = document.querySelector('#mapFrame');
let pinElement = document.querySelector('#one'); //changed to #one
pinElement.addEventListener('click', function (e) {
imgElement.style.transform = 'translate(-' + 0 + 'px,-' + 0 + 'px) scale(2)';
pinElement.style.display = 'none';
event.stopPropagation(); //added to prevent bubbling
});
imgElement.addEventListener('click', function (e) {
imgElement.style.transform = null;
pinElement.style.display = 'block';
});

How do I make fabric.js output svg including alpha color?

I need to output the result of a fabric canvas drawing to an SVG file.
I'm using fabric.js version 1.7.6 and when I have a path drawn to a canvas with an rgba fill like rgba(255,0,0,.15) the resulting SVG has a fill of rgb(0,0,0). Is there some setting I need to enable to make it output the alpha chanel?
In my sample code the purple circle converts to SVG properly, but the rectangle just shows up as black.
Sample HTML:
<html>
<head>
<script src="fabric.js"></script>
</head>
<body>
<div id="canvasHolder" style="border: 3px solid black;">
<canvas id="canvasElement" width="400" height="400" />
</div>
<div id="svgHolder" style="border: 3px solid blue;">
</div>
</body>
<script>
var canvas = new fabric.Canvas('canvasElement');
var rect = new fabric.Path('M,0,0,h,100,v,100,h,-100,z',{
top:100,
left:100,
stroke: 'green',
fill: 'rgba(255,0,0,.15)'
});
canvas.add(rect);
var circ = new fabric.Circle({
radius: 30,
top:30,
left:30,
stroke: 'blue',
fill: 'purple'
});
canvas.add(circ);
canvas.renderAll();
// Make an SVG object out of the fabric canvas
var SVG = canvas.toSVG();
document.getElementById('svgHolder').innerHTML = SVG;
</script>
</html>
Output SVG:
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="400" height="400" viewBox="0 0 400 400" xml:space="preserve">
<desc>Created with Fabric.js 1.7.6</desc>
<defs>
</defs>
<path d="M 0 0 h 100 v 100 h -100 z" style="stroke: rgb(0,128,0); stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" transform="translate(150.5 150.5) translate(-50, -50) " stroke-linecap="round"></path>
</svg>
As I said in comment, this looks like a bug, that you should report on the project's issue tracker.
Colors are all converted to rgb() (rgba, hsl, hsla, hex, keywords) and thus don't support alpha channel...
For the time being, here is an heavy workaround :
toSVG accepts an reviver function, which will receive all the svg nodes markups. From there, you can reapply the correct styles, but not so easily.
The only parameter I could find allowing us to identify which object corresponds to the svg markup we get, is the id one.
So first, we will construct a dictionary, which will store our colors, by id.
Then, we will assign the id and colors to our fabric's objects.
Finally, in the reviver, we will parse our markup to convert it to an svg node, check its id atribute, and then change its style.fill and style.stroke properties, before returning the serialization of this modified node.
var canvas = new fabric.Canvas('canvasElement');
var colors_dict = {};
// an helper function to generate and store our colors objects
function getColorId(fill, stroke) {
var id = 'c_' + Math.random() * 10e16;
return colors_dict[id] = {
id: id,
fill: fill || 'black',
stroke: stroke || 'black' // weirdly fabric doesn't support 'none'
}
}
// first ask for the color object of the rectangle
var rect_color = getColorId('hsla(120, 50%, 50%, .5)', 'rgba(0,0,255, .25)');
var rect = new fabric.Path('M,0,0,h,100,v,100,h,-100,z', {
top: 60,
left: 60,
stroke: rect_color.stroke, // set the required stroke
fill: rect_color.fill, // fill
id: rect_color.id // and most importantly, the id
});
canvas.add(rect);
var circ_color = getColorId('rgba(200, 0,200, .7)');
var circ = new fabric.Circle({
radius: 30,
top: 30,
left: 30,
stroke: circ_color.stroke,
fill: circ_color.fill,
id: circ_color.id
});
canvas.add(circ);
canvas.renderAll();
var parser = new DOMParser();
var serializer = new XMLSerializer();
function reviveColors(svg){
// first we parse the markup we get, and extract the node we're interested in
var svg_doc = parser.parseFromString('<svg xmlns="http://www.w3.org/2000/svg">' + svg + '</svg>', 'image/svg+xml');
var svg_node = svg_doc.documentElement.firstElementChild;
var id = svg_node.getAttribute('id');
if (id && id in colors_dict) { // is this one of the colored nodes
var col = colors_dict[id]; // get back our color object
svg_node.style.fill = col.fill; // reapply the correct styles
svg_node.style.stroke = col.stroke;
// return the new markup
return serializer.serializeToString(svg_node).replace('xmlns="http://www.w3.org/2000/svg', '');
}
return svg;
}
// Make an SVG object out of the fabric canvas
var SVG = canvas.toSVG(null, reviveColors);
document.getElementById('svgHolder').innerHTML = SVG;
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.7.12/fabric.js"></script>
<div id="canvasHolder" style="border: 3px solid black;">
<!-- beware canvas tag can't be self-closing -->
<canvas id="canvasElement" width="400" height="200"></canvas>
</div>
<div id="svgHolder" style="border: 3px solid blue;">
The colors are converted to RGB because svg specs wants color in CSS2 format and so rgba is unsupported.
cit: https://www.w3.org/TR/2008/REC-CSS2-20080411/syndata.html#value-def-color
Fabric fulfill the transparency with the fill-opacity rull. The point is that fabric.Color color parser looks like is choking over the .15 notation for the alpha channel.
Please use 0.15 and it will work.
i agree that fabric.Color could be fixed for this.

Use SVG icon as marker in OpenLayers

I tried to svg icon as marker in Openlayers-3. Here in my code.
var svg = '<?xml version="1.0"?>'
+ '<svg viewBox="0 0 120 120" version="1.1" xmlns="http://www.w3.org/2000/svg">'
+ '<circle cx="60" cy="60" r="60"/>'
+ '</svg>';
var style = new ol.style.Style({
image: new ol.style.Icon({
opacity: 1,
src: 'data:image/svg+xml;base64,' + btoa(svg)
})
});
But my svg image is truncated,as shown in the following picture. (
the icon should be a circle)
Here is an example that shows inline SVG in an icon symbolizer: http://jsfiddle.net/eze84su3/
Here is the relevant code:
var svg = '<svg width="120" height="120" version="1.1" xmlns="http://www.w3.org/2000/svg">'
+ '<circle cx="60" cy="60" r="60"/>'
+ '</svg>';
var style = new ol.style.Style({
image: new ol.style.Icon({
opacity: 1,
src: 'data:image/svg+xml;utf8,' + svg,
scale: 0.3
})
});
A few differences from yours:
I added width and height attributes to the <svg>. This lets the browser know how big to make the resulting image.
I added a scale property to the icon to resize the image.
I used utf8 instead of base64 encoding (not significant).
To me, the solution was:
const iconMarkerStyle = new ol.style.Icon({
src: './data/static_images/marker.svg',
//size: [100, 100],
offset: [0, 0],
opacity: 1,
scale: 0.35,
//color: [10, 98, 240, 1]
})
Then add size parameters directly in the SVG file:
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 144.81 215.81" width="14.5px" height="21.6px">
<title>Asset 190-SVG</title>
</svg>
I am using svg icon as image src in open layer styles.
var custstyle = new ol.style.Style({
image: new ol.style.Icon({
anchor: [0.5, 50],
anchorXUnits: 'fraction',
anchorYUnits: 'pixels',
src: './Icon.svg',
}),
});

Maintain orientation of some elements in dynamic rotation?

I'm animating rotations of groups of SVG elements using d3.js. However, I want to preserve the orientation of some elements. For example, in this fiddle (code below), the blue dot is the center of the blue circle. The blue dot is displaced vertically from the black dot, which rotates around the yellow center. I want to maintain the vertical relationship between these two dots.
In the fiddle, I maintain the vertical orientation by rotating the "shift" <g> group backwards the same amount that its enclosing group is rotating forwards. This is the same method given in cmonkey's answer here. That works, but I'm wondering whether there are other methods. Is there any way to preserve orientation without an inverse rotation?
Why?
The inverse rotation strategy means that one has to carefully keep the inverse rotations in sync with changes to rotations of outer groups. In full-fledged versions of this code, I use rotations within rotations (within rotations), as in this example. That means summing up all of the outer groups' rotations in order to determine what the inverse rotation should be. I also want to add text labels to SVG elements. Since different text labels will fall within different numbers of rotation groups, each text label will need its own customized rotation if I want to keep the text upright.
Feel free to suggest more D3. I only hand-coded the SVG in these versions in order to get clarity about how I would dynamically generate the SVG with D3 in a later version.
<!DOCTYPE html>
<html>
<head>
<script src="http://d3js.org/d3.v3.min.js"></script>
<style>
.cycle {
stroke : #000000;
fill : none;
}
.movingPointOnCycle {
stroke : #000000;
fill : #000000;
}
#pointB {
stroke : #0000FF;
fill : #0000FF;
}
#epicycle2 {
stroke : #0000FF;
}
.centerOfUniverse {
stroke : #000000;
fill : #000000;
}
.sun {
stroke : #000000;
fill : #F0F000;
}
.mars {
stroke : #000000;
fill : #A00000;
}
.earth {
stroke : #000000;
fill : #00A000;
}
</style>
</head>
<body>
<svg width="400" height="400">
<g transform="translate(200,200) scale(1.3)">
<circle class="sun" r="10"></circle>
<g class="cycle"speed="0.01">
<circle id="deferent" class="cycle" r="60"></circle>
<g class="epicycleCenter" transform="translate(60,0)">
<circle id="pointD" class="movingPointOnCycle" r="2"></circle>
<g class="shift" speed="-0.01" displacement="-25">
<circle id="pointB" class="movingPointOnCycle" r="2"></circle>
<line x1="0" y1="0" x2="0" y2="25" stroke-dasharray="1,2"></line>
<g class="cycle"speed="0.01">
<circle id="epicycle2" class="cycle" r="75"></circle>
</g>
</g>
</g>
</g>
</g>
</svg>
<script type="text/javascript">
var t0 = Date.now();
var svg = d3.select("svg");
d3.timer(function() {
var delta = (Date.now() - t0);
svg.selectAll(".cycle").attr("transform", function(d) {
return "rotate(" + delta * d3.select(this).attr("speed") + ")";
});
svg.selectAll(".shift").attr("transform", function(d) {
return "rotate(" + delta * d3.select(this).attr("speed") + ")"
+
"translate(0," + d3.select(this).attr("displacement") + ")"
;
});
});
</script>
</body>
</html>

Resources