How to compute getBoundingClientRect() in a scaled svg? - javascript

I have a usecase where designers supply us with a SVG, and we use certain elements in that SVG to position our dynamically created elements.
In the snippet below I try to overlap the rect#overlayTarget with the div#overlay using getBoundingClientRect: it doesn't take the scaling of the parent element into account, and the elements don't overlap.
The answers from this question is not applicable here as it uses element.offsetLeft and element.offsetTop, which aren't available for SVG: How to compute getBoundingClientRect() without considering transforms?
How do I make the #overlay and #overlayTarget overlap?
const target = document.querySelector("#overlayTarget");
const position = target.getBoundingClientRect();
const overlay = document.querySelector("#overlay");
overlay.style.top = `${position.y}px`;
overlay.style.left = `${position.x}px`;
overlay.style.width = `${position.width}px`;
overlay.style.height = `${position.height}px`;
#overlay {
position: absolute;
background: hotpink;
opacity: 0.3;
width: 100px;
height: 100px;
}
<div id="app" style="transform: scale(0.875);">
Test
<div id="overlay"></div>
<svg xmlns="http://www.w3.org/2000/svg" width="1809" height="826" viewBox="0 0 809 826">
<g
id="Main_overview"
data-name="Main overview"
transform="translate(-49.5 -155)"
>
<g
id="overlayTarget"
data-name="DC-DC converter"
transform="translate(400 512)"
>
<rect
id="Rectangle_29"
data-name="Rectangle 29"
width="74"
height="74"
fill="none"
stroke="#47516c"
stroke-width="2"
/>
</g>
</g>
</svg>
</div>

If you cannot set your overlay element outside of the transformed element, this answer will work, but only for some simple transformations:
translations and
scales with factors > 0
In these cases, the corners of the bounding box aren't moved out of their top/left and bottom/right orientation. Rotations or skews, and most of the 3D transforms won'T work out.
You can then compute the resulting box values for your overlay by transforming the corners of position with the inverse matrix to that set for the #app element. The DOMPoint and DOMMatrix interfaces help with that.
It is important to remember that transform sets an implicit position: relative, so the top and left values of the overlay are not in relation to the viewport.
const app = document.querySelector('#app');
const relative = app.getBoundingClientRect();
const target = document.querySelector("#overlayTarget");
const position = target.getBoundingClientRect();
const matrix = new DOMMatrix(app.style.transform).inverse();
const topleft = new DOMPoint(
position.x - relative.x,
position.y - relative.y
).matrixTransform(matrix);
const bottomright = new DOMPoint(
position.x - relative.x + position.width,
position.y - relative.y + position.height
).matrixTransform(matrix);
const overlay = document.querySelector("#overlay");
overlay.style.top = `${topleft.y}px`;
overlay.style.left = `${topleft.x}px`;
overlay.style.width = `${bottomright.x - topleft.x}px`;
overlay.style.height = `${bottomright.y - topleft.y}px`;
#overlay {
position: absolute;
background: hotpink;
opacity: 0.3;
width: 100px;
height: 100px;
}
<div id="app" style="transform: scale(0.875);">
Test
<div id="overlay"></div>
<svg xmlns="http://www.w3.org/2000/svg" width="1809" height="826" viewBox="0 0 809 826">
<g
id="Main_overview"
data-name="Main overview"
transform="translate(-49.5 -155)"
>
<g
id="overlayTarget"
data-name="DC-DC converter"
transform="translate(400 512)"
>
<rect
id="Rectangle_29"
data-name="Rectangle 29"
width="74"
height="74"
fill="none"
stroke="#47516c"
stroke-width="2"
/>
</g>
</g>
</svg>
</div>

Related

Coloring a straight path from X1=0 to X2=100, depending on user inputs

I'm making a progressive line bar using SVG elements, my logic is that for every number of inputs , the line gets divided on even parts and each part will get colored by percentage, depending of the number of valid interactions.
In the console I can see the logic is working as it should, however the coloring of the line is not.
I'm not fully sure how can I make that the progress gets colored from left to right progressively, right now its behaviour doesn't follow the desired path.
function updateBar() {
//Path that will be painted
var myProgress = document.getElementById("myProgress");
//Reference the number of inputs
var numberOfInputs =document.getElementById("totalInput").value;
//number that we will use to divide the valid interaction with the inputs
var interactionTimes = document.getElementById("validInput").value;
//getting the # to get the right percent to paint the line
var lineDivision = [(100 / numberOfInputs)];
console.log("this is lineDivision:" + lineDivision);
//and then with this lineDivision variable, replace the 20 from the previous code. But here is where I have the main problem, because as result of every click I do in a checkbox the var percent always get 100 and as result the ring is already fully colored.
var percent = (interactionTimes) * (lineDivision);
console.log("this is the percent:" + percent);
myProgress.style.strokeDashoffset = 100 - percent;
if (interactionTimes === numberOfInputs) {
myProgress.style.stroke = "#1DEBA1";
} else if (interactionTimes < numberOfInputs) {
myProgress.style.stroke = "purple";
}
return true;
}
checks = document.querySelectorAll("input[type='number']");
checks.forEach(function(paint) {
paint.addEventListener("change", function() {
updateBar()
});
});
#mySvg {
transform: rotate(-180deg);
}
#myRect {
width: 0;
height: 30px;
stroke: #E3E5E7;
stroke-width: 3px;
}
#myProgress {
margin: auto;
width: 50%;
/*stroke: red;*/
stroke-width: 3px;
stroke-dasharray: 100;
stroke-dashoffset: 100;
stroke-linecap: square;
}
<svg id="line-progress" height="4" width="300">
<g >
<line class="myRect" id="myRect" x1="0" y1="50%" x2="100%" y2="50%" stroke-width="4" fill="transparent" />
</g>
<g >
<line class="myProgress" id="myProgress" x1="0" y1="50%" x2="100%" y2="50%" pathLength="100" fill="transparent"/>
</g>
</svg>
<form>
<label> Total inputs to interact with
<input id="totalInput" type="number" min="0" max="100">
</label>
<br>
<label> Number of valid inputs
<input id="validInput" type="number" min="0" max="100">
</label>
</form>
<script src="script.js"></script>

matrixTransform on svg: unexpected behaviour

I have a div containing an SVG image of 300x300 px and a viewbox of 1000x1000.
The image describes a blue rectangle on top of a red one.
When I move the mouse a circle is following the mouse position inside the image:
Everything is perfect except that when I apply a transformation changing perspective and rotation, the mouse pointer and circle center are not anymore matching:
Code is here:
$(function() {
$('#image').mousemove(function(event) {
var svg = document.querySelector('svg');
var pt = svg.createSVGPoint();
pt.x = event.clientX;
pt.y = event.clientY;
pt = pt.matrixTransform(svg.getScreenCTM().inverse());
overlay = document.getElementById('overlay');
$('#overlay').html(
"<circle cx='" + pt.x + "' cy='" + pt.y + "' r='50' stroke='#8f00ff' fill='transparent' stroke-width='10' /></svg>"
);
refresh = $("#overlay").html();
$("#overlay").html( refresh )
});
});
function Transform() {
$('#image').css({
transformOrigin: '500px 500px',
transform: 'perspective(100px) rotateX(5deg)'
});
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id='image' tabindex='0' >
<svg id='svgmap' width='300' height='300' xmlns='http://www.w3.org/2000/svg' version='1.1' xmlns:xlink='http://www.w3.org/1999/xlink' viewBox='0 0 1000 1000'>
<rect x='0' y='0' width='1000' height='1000' fill='red' />
<rect x='250' y='250' width='500' height='500' stroke='yellow' fill='blue' stroke-width='10' />
<g id='overlay'></g>
</svg>
</div>
<button onclick='Transform()'>Transform</button>
My goal is to preserver matching between the purple circle center and the mouse pointer, even when a transformation is applied to the object.
Is there a way to do it?
In your code #image is a div. In order to make it work you need to apply the transformation to the svg element (#svgmap) and the transformation must be an svg transformation.
$(function() {
$('#svgmap').mousemove(function(event) {
var svg = document.querySelector('svg');
var pt = svg.createSVGPoint();
pt.x = event.clientX;
pt.y = event.clientY;
pt = pt.matrixTransform(svg.getScreenCTM().inverse());
overlay = document.getElementById('overlay');
$('#overlay').html(
"<circle cx='" + pt.x + "' cy='" + pt.y + "' r='50' stroke='#8f00ff' fill='transparent' stroke-width='10' /></svg>"
);
refresh = $("#layer_wafer").html();
$("#layer_wafer").html( refresh )
});
});
function Transform() {
svgmap.setAttributeNS(null,"transform", "skewX(-20) translate(100)");
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id='image' tabindex='0' >
<svg id='svgmap' width='300' height='300' xmlns='http://www.w3.org/2000/svg' version='1.1' xmlns:xlink='http://www.w3.org/1999/xlink' viewBox='0 0 1000 1000' transform="">
<rect x='0' y='0' width='1000' height='1000' fill='red' />
<rect x='250' y='250' width='500' height='500' stroke='yellow' fill='blue' stroke-width='10' />
<g id='overlay'></g>
</svg>
</div>
<button onclick='Transform()'>Transform</button>
I understand that you are needing a 3D css transformation but this (at least for now) doesn't work.
This is an article where you can read more about 3d transforms in svg: https://oreillymedia.github.io/Using_SVG/extras/ch11-3d.html In the article you can read: All the 3D transformation functions described in this section should be considered “future”
I've solved the issue in the ugliest possible way. Simply hiding the mouse cursor over the div.
Ugly. But effective.
$(function() {
$('#image').mousemove(function(event) {
var svg = document.querySelector('svg');
var pt = svg.createSVGPoint();
pt.x = event.clientX;
pt.y = event.clientY;
pt = pt.matrixTransform(svg.getScreenCTM().inverse());
overlay = document.getElementById('overlay');
$('#overlay').html(
"<circle cx='" + pt.x + "' cy='" + pt.y + "' r='50' stroke='#8f00ff' fill='transparent' stroke-width='10' /></svg>"
);
refresh = $("#overlay").html();
$("#overlay").html( refresh )
});
});
function Transform() {
$('#image').css({
transformOrigin: '500px 500px',
transform: 'perspective(100px) rotateX(5deg)'
});
}
div#image {
cursor: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id='image' tabindex='0' >
<svg id='svgmap' width='300' height='300' xmlns='http://www.w3.org/2000/svg' version='1.1' xmlns:xlink='http://www.w3.org/1999/xlink' viewBox='0 0 1000 1000'>
<rect x='0' y='0' width='1000' height='1000' fill='red' />
<rect x='250' y='250' width='500' height='500' stroke='yellow' fill='blue' stroke-width='10' />
<g id='overlay'></g>
</svg>
</div>
<button onclick='Transform()'>Transform</button>
There's a little bit of optimization that I have to do to reduce the mouse cursor position gap between the DIV over and any other page element, but it's acceptable to me.

Snap.SVG - Manipulate $(this) element

I have a problem with Snap.SVG and animation of multiple SVG elements.
I want to change path on hover, but i have many same SVG in html.
HTML:
var svg = $('.svg-wave');
var s = Snap(svg);
var simpleCup = Snap.select('.svg-wave-normal');
var fancyCup = Snap.select('.svg-wave-hover');
var simpleCupPoints = simpleCup.node.getAttribute('d');
var fancyCupPoints = fancyCup.node.getAttribute('d');
svg.mouseenter(function() {
simpleCup.animate({
d: fancyCupPoints
}, 600);
}).mouseleave(function() {
simpleCup.animate({
d: simpleCupPoints
}, 600);
});
svg .svg-wave-hover {opacity: 0;}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/snap.svg/0.5.1/snap.svg-min.js"></script>
<div class="item">
<svg class="svg-wave" width="240" height="120" viewBox="0 0 240 120" xmlns="http://www.w3.org/2000/svg">
<path class="svg-wave-normal" d="M108.5,114.8C71.7,114.8,62.7,117,0,117h217C151,117,146.2,114.8,108.5,114.8" fill="#69c6d3"></path>
<path class="svg-wave-hover" d="M108.5,0C71.7,0,62.7,117,0,117h217C151,117,146.2,0,108.5,0" fill="#69c6d3"></path>
</svg>
</div>
<div class="item">
<svg class="svg-wave" width="240" height="120" viewBox="0 0 240 120" xmlns="http://www.w3.org/2000/svg">
<path class="svg-wave-normal" d="M108.5,114.8C71.7,114.8,62.7,117,0,117h217C151,117,146.2,114.8,108.5,114.8" fill="#69c6d3"></path>
<path class="svg-wave-hover" d="M108.5,0C71.7,0,62.7,117,0,117h217C151,117,146.2,0,108.5,0" fill="#69c6d3"></path>
</svg>
</div>
<div class="item">
<svg class="svg-wave" width="240" height="120" viewBox="0 0 240 120" xmlns="http://www.w3.org/2000/svg">
<path class="svg-wave-normal" d="M108.5,114.8C71.7,114.8,62.7,117,0,117h217C151,117,146.2,114.8,108.5,114.8" fill="#69c6d3"></path>
<path class="svg-wave-hover" d="M108.5,0C71.7,0,62.7,117,0,117h217C151,117,146.2,0,108.5,0" fill="#69c6d3"></path>
</svg>
</div>
The problem is that when i hover last SVG, it animate the first one.
Can someone help me to change mouseenter/leave to work from $(this) ?
I'm going to suggest a way without using JQuery firstly, as it's not really necessary.
I would change your css to make the hover path display: none, rather than opacity: 0, as display none means that events will pass through fine, whereas opacity will capture them. Or you could reorder the paths, so the hove paths come first.
var normalWaves = Snap.selectAll('.svg-wave-normal');
var normalPoints = Snap.select('.svg-wave-normal').attr('d');
var hoverPoints = Snap.select('.svg-wave-hover').attr('d');
normalWaves.forEach(function( wave ) {
wave.mouseover(function() {
this.animate({
d: hoverPoints
}, 600);
})
.mouseout(function() {
this.animate({
d: normalPoints
}, 600);
});
});
svg .svg-wave-hover { display: none; }
jsfiddle

Works in codepen but not in Wordpress

I'm trying to get this animated svg tree to work in Wordpress. It works fine in codepen, but not at all in a Wordpress page on my localhost.
Can anyone see what is missing/wrong? In the page source code the javascript files are loading.
svg tree
var svg = $("#svg-container");
svg.children().find("path:not(.except)").click(function(e) {
$("#Layer_1 path").removeAttr("style");
$this = $(this);
var bbox = this.getBBox();
var centreX = bbox.x + bbox.width / 2;
var centreY = bbox.y + bbox.height / 2;
$this.css("transform-origin", centreX + 'px ' + centreY + 'px');
$this.css("transform", "scale(4)");
$this.css("stroke", "");
$this.css("fill", "");
this.parentElement.appendChild(this);
})
#svg-container {
width: 500px;
height: 500px;
}
#svg-container svg {
width: 100%;
height: 100%;
}
#font-face {
font-family: "Amaranth";
src: url('https://fonts.googleapis.com/css?family=Amaranth');
}
<div id="svg-container">
<!--?xml version="1.0" encoding="utf-8"?-->
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<script type="text/javascript">
< ![CDATA[
]] >
</script>
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="1247.24px" height="1360.63px" viewBox="0 0 1800 1400" enable-background="new 0 0 1247.24 1360.63" xml:space="preserve">
<g>
<font>
<font-face font-family="Amaranth">
<font-face-src>
<font-face-uri xlink:href="https://fonts.googleapis.com/css?family=Amaranth" />
</font-face-src>
</font-face>
</font>
<text x="10" y="10" font-weight="bold" font-size="75" fill="#3ABDF2" font-family="Amaranth">The tree</text>
<path class="except" fill="#3E6325" d="m1175 917.29c-11.44-1.847-21.576 0.042-32.652 2.825-3.182 0.8-6.644 1.598-10.131 1.985 48.901-29.163 .....continued"
fill="#639357"
id="path5022" /><path
d="m604.1 171.56s-18.18-9.487-18.989-9.087c-0.812 0.401-2.108 1.365-0.619 2.624 1.491 1.259 18.873 8.725 20.208 8.689 1.331-0.037 1.5-1.57-0.6-2.226z"
fill="#3E6325"
stroke="#A64F2C"
stroke-miterlimit="10"
id="path5024" />
</g>
</svg>
</div>
https://codepen.io/paulfadams/pen/PRzMNE?editors=1111
I had this same issue once.
WordPress ships with its own version of the jQuery library.
Try using "jQuery" instead of just "$" sign.
For example:
var svg = $("#svg-container"); should be replaced with var svg = jQuery("#svg-container");
Use jQuery instead of the "$" sign. ex: jQuery("#svg-container");

Get position of svg element in svg with multiple transforms

I have a SVG-document that contains a circle. I would like to place an absolutely positioned html-element on top of this circle. The circle contains a cx and cy and there are several transforms on parent elements. How can I translate the position inside the SVG to the coordinate space of the parent html-element (normal pixels)?
The SVG is generated by a program, so I will have little control over it, meaning that I will need a general solution that can handle any number of transforms on parent element.
I'm not using d3 or any similar library, so I'm looking for a way to solve this using plain JavaScript. This is a simplified example of my problem:
<head>
<style>
#box {
background-color: greenyellow;
position: absolute;
width: 100px;
height: 100px;
}
</style>
</head>
<body>
<div id="root">
<svg width="1274.2554" height="692.35712" id="svg">
<g id="viewport" transform="matrix(0.655937472561484,0,0,0.655937472561484,162.2578890186666,57.23719435833714)">
<g id="layer1" transform="translate(-5.49584,-171.51931)">
<g id="elem" transform="translate(34.862286,232.62127)">
<circle id="center" cx="19.952429" cy="19.90885" style="fill:#ffcc33" r="20"></circle>
</g>
</g>
</g>
</svg>
<div id="box" ></div>
</div>
<script>
function positionBoxAtCenter() {
var box = document.getElementById('box');
var center = document.getElementById(('center'));
// TODO: get x, y from 'center' so that 'box' can be placed on top of center
var x = 0, y = 0;
box.setAttribute('style', 'top: ' + y + 'px; left: ' + x + 'px');
}
</script>
</body>
JSFiddle: https://jsfiddle.net/p9pf56sz/
Using getBoundingClientRect():
var pos = center.getBoundingClientRect();
You can get the center of the circle:
var x = pos.left + pos.width/2, y = pos.top + pos.height/2;
Here is the demo:
function positionBoxAtCenter() {
var box = document.getElementById('box');
var center = document.getElementById(('center'));
var pos = center.getBoundingClientRect();
var x = pos.left + pos.width/2, y = pos.top + pos.height/2;
box.setAttribute('style', 'top: ' + y + 'px; left: ' + x + 'px');
}
positionBoxAtCenter();
#box {
background-color: greenyellow;
position: absolute;
width: 100px;
height: 100px;
}
<body>
<div id="root">
<svg width="1274.2554" height="692.35712" id="svg">
<g id="viewport" transform="matrix(0.655937472561484,0,0,0.655937472561484,162.2578890186666,57.23719435833714)">
<g id="layer1" transform="translate(-5.49584,-171.51931)">
<g id="elem" transform="translate(34.862286,232.62127)">
<circle id="center" cx="19.952429" cy="19.90885" style="fill:#ffcc33" r="20"></circle>
</g>
</g>
</g>
</svg>
<div id="box" ></div>
</div>
</body>
However, as you can see in the demo, you are moving the top/left corner of the rectangle (its origin) to the center of the circle. So, you'll have to calculate the center of the rectangle as well (and subtract its difference to the origin).

Categories