I have a SVG shape (which is quite complex, I have made it simple for the sake of understanding) which I need to transform. My requirement is,
When mouseover, The group should be scaled to 2x
When mouseout, group should scale back to 1x
When dragging the group scale should be preseved
So far, I have managed to do all the parts, except one issue. After I drag the element when I try to mouseover the group it reverts to its original location. I cannot understand why it happens. Here is a working fiddle. Can anyone help me out?
index.html
<svg width="400" height="400" style="background-color: red">
<g id="op" class="operator" transform="translate(0,0)">
<circle class="head" cx="50" cy="50" r="20" style="fill: yellow"></circle>
<circle cx="40" cy="40" r="10" style="fill: blue"></circle>
<circle cx="60" cy="40" r="10" style="fill: blue"></circle>
</g>
</svg>
script.js
d3.selectAll('.operator')
.on('mouseenter', function () {
console.log('Mouse Enter');
var c = d3.select(this).select('.head');
var x = c.attr('cx');
var y = c.attr('cy');
var scale = 2;
var scaleX = -1 * x * (scale - 1);
var scaleY = -1 * y * (scale - 1);
d3.select(this).attr('transform', 'translate(' + scaleX + ',' + scaleY + ')' + 'scale(' + scale + ')');
})
.on('mouseleave', function () {
console.log('Mouse Leave');
var c = d3.select(this).select('.head');
var x = c.attr('cx');
var y = c.attr('cy');
var scale = 1;
var scaleX = -1 * x * (scale - 1);
var scaleY = -1 * y * (scale - 1);
d3.select(this).attr('transform', 'translate(' + scaleX + ',' + scaleY + ')' + 'scale(' + scale + ')');
})
.call(d3.behavior.drag()
.origin(function () {
var t = d3.select(this);
return {
x: d3.transform(t.attr("transform")).translate[0],
y: d3.transform(t.attr("transform")).translate[1]
};
})
.on('drag', function () {
var oldScale = d3.transform(d3.select(this).attr('transform')).scale;
d3.select(this).attr('transform', 'translate(' + d3.event.x + ',' + d3.event.y + ')scale(' + oldScale + ')');
}))
As #PhilAnderson said, you shouldn't be mixing translate and cx/cy. In fact, the way you are nesting elements, you should only be translating. Translate your g within the SVG and then translate your circles within the g. Correcting that, things get much simpler:
<!DOCTYPE html>
<html>
<head>
<script data-require="d3#3.5.3" data-semver="3.5.3" src="//cdnjs.cloudflare.com/ajax/libs/d3/3.5.3/d3.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="script.js"></script>
</head>
<body>
<svg width="400" height="400" style="background-color: red">
<g id="op" class="operator" transform="translate(50,50)">
<circle class="head" r="20" style="fill: yellow"></circle>
<circle transform="translate(-10,0)" r="10" style="fill: blue"></circle>
<circle transform="translate(10,0)" r="10" style="fill: blue"></circle>
</g>
</svg>
<script>
d3.select('.operator')
.on('mouseenter', function() {
console.log('Mouse Enter');
var self = d3.select(this),
xy = d3.transform(self.attr('transform')).translate,
scale = 2;
self.attr('transform', 'translate(' + xy[0] + ',' + xy[1] + ')' + 'scale(' + scale + ')');
})
.on('mouseleave', function() {
console.log('Mouse Leave');
var self = d3.select(this),
xy = d3.transform(self.attr('transform')).translate,
scale = 1;
self.attr('transform', 'translate(' + xy[0] + ',' + xy[1] + ')' + 'scale(' + scale + ')');
})
.call(d3.behavior.drag()
.on('drag', function() {
var self = d3.select(this),
oldScale = d3.transform(self.attr('transform')).scale;
self.attr('transform', 'translate(' + d3.event.x + ',' + d3.event.y + ')scale(' + oldScale + ')');
}))
</script>
</body>
</html>
It looks like you're confusing the cx and cy attributes with the translate.
In mouseenter and mouseleave you move the shape according to the values of cx and cy, but in the drag event you simply translate using the x and y values of the event.
One way of fixing this would be to set the cx and cy attributes in your drag event, although tbh it would be better to settle for one approach and stick to it throughout.
Related
I am trying to have an svg with internal javascript, see here <"https://jsfiddle.net/melvinT/boaus7k1/76/">jsfiddle of svg draggable triangle path it is three circles that drag a triangle. But there is just a bit that stubbornly stop working when I move it into the svg, and I can not figure out why?
The section that mysteriously stops moving when I drag it into the svg is the following (it is at the end) Why? How can I move it into the svg?
var elements = [p1, p2, p3,];
for (i = 0; i < elements.length; i++) {
elements[i].addEventListener('mousedown', onMouseDown);
}
If you put the script element at the bottom of your SVG then it should work because then it will run after the circle elements have been loaded.
<svg id="figure1" width="500" height="500">
<style>
#figure1 {
border: 1px solid blue;
}
</style>
<path d="M170 150 20 110 250 60 z" stroke="pink" fill="none" id="controlPath"/>
<circle cx="170" cy="150" r="8" id="p1" fill="red" style="cursor: move;"/>
<circle cx="20" cy="110" r="8" id="p2" fill="black" style="cursor: move;"/>
<circle cx="250" cy="60" r="8" id="p3" fill="lightgrey" style="cursor: move;"/>
<script type="text/javascript">
<![CDATA[
var svg = document.getElementById('figure1');
svg.addEventListener('mousemove', onMouseMove);
svg.addEventListener('mouseup', onMouseUp);
var controlPath = document.getElementById('controlPath');
var dragElem = null;
var p1 = document.getElementById('p1');
var p2 = document.getElementById('p2');
var p3 = document.getElementById('p3');
var elements = [p1, p2, p3,];
for (i = 0; i < elements.length; i++) {
elements[i].addEventListener('mousedown', onMouseDown);
}
function onMouseMove(ev) {
var p;
if (!dragElem) {
return;
}
p = getClientPointInSVG(ev);
dragElem.setAttribute('cx', p.x - mouseOffsetX);
dragElem.setAttribute('cy', p.y - mouseOffsetY);
controlPath.setAttribute('d',
'M' + p1.getAttribute('cx') + ' ' + p1.getAttribute('cy') +
' ' + p2.getAttribute('cx') + ' ' + p2.getAttribute('cy') +
' ' + p3.getAttribute('cx') + ' ' + p3.getAttribute('cy') +
' ' + 'z' );
}
function onMouseDown(ev) {
var p;
dragElem = ev.target;
p = getClientPointInSVG(ev);
mouseOffsetX = p.x - dragElem.getAttribute('cx');
mouseOffsetY = p.y - dragElem.getAttribute('cy');
}
function getClientPointInSVG(ev) {
var p, m;
p = svg.createSVGPoint();
p.x = ev.clientX;
p.y = ev.clientY;
m = dragElem.getScreenCTM();
return p.matrixTransform(m.inverse());
}
function onMouseUp(ev) {
dragElem = null;
}
var mouseOffsetX, mouseOffsetY;
]]>
</script>
</svg>
About half way down on the following site there is an image of a house with SVG animations and hotspots.
https://enphase.com/en-us/homeowners
I see all the individual elements but I don't understand how the they put it all together. The elements are positioned using percentages to 5 decimal places. I'm assuming they used some software to create the SVGs put more importantly, the layout. Any idea what that software is? There is now way they hand coded the layout and calculated the positioning.
You can have the same functionality using SVG stroke-dasharray and stroke-dashoffset attributes mainipulation with javascript animation timers, I usually use D3.js to do this type of animation/SVG manipulation, but you can also do it purely in javascript, here is a block example by Noah Veltman on bl.ocks.org:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<style>
path {
fill: none;
stroke: #d3008c;
stroke-width: 2px;
}
#arrowhead {
fill: #d3008c;
stroke: none;
}
</style>
</head>
<body>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="960" height="500">
<path d="M636.5,315c-0.4-18.7,1.9-27.9-5.3-35.9
c-22.7-25-107.3-2.8-118.3,35.9c-7,24.4,20.6,37.2,16,71c-4,29.6-30.8,60.7-56.5,61.1c-30.8,0.4-32.9-43.8-81.7-70.2
c-50.9-27.6-110.1-12.9-125.2-9.2c-66.1,16.4-82.2,56.9-109.2,47.3c-38-13.6-55.9-112.1-19.8-143.5c39-34,121.2,27.7,148.1-3.8
c18-21.1,3.1-74.3-25.2-105.3c-31.1-34.1-70.1-32.4-105.3-76.3c-8.2-10.2-16.9-23.8-15.3-39.7c1.2-11.4,7.5-23.3,15.3-29
c33.8-25,101.6,62.6,193.1,59.5c40.1-1.3,38.7-18.5,99.2-38.9c126.2-42.6,242.4-4.9,297.7,13c54.7,17.7,105.4,35,129.8,82.4
c13,25.3,22.9,67.7,4.6,87c-11.6,12.3-25.1,5.1-46.6,20.6c-2.8,2-28.9,21.4-32.1,49.6c-3.1,27.4,18.7,35,29,70.2
c8.8,30.1,8.5,77.8-18.3,99.2c-32.3,25.8-87,0.6-100-5.3c-69.6-32-67.2-88.4-73.3-109.2z"/>
<defs>
<path id="arrowhead" d="M7,0 L-7,-5 L-7,5 Z" />
</defs>
</svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var path = document.querySelector("path"),
totalLength = path.getTotalLength(),
group = totalLength / 20,
start;
var arrowheads = d3.select("svg").selectAll("use")
.data(d3.range(20).map(function(d){ return d * group + 50; }))
.enter()
.append("use")
.attr("xlink:href", "#arrowhead");
path.style.strokeDasharray = "50," + (group - 50);
requestAnimationFrame(update);
function update(t) {
if (!start) {
start = t;
}
var offset = -group * ((t - start) % 900) / 900;
path.style.strokeDashoffset = offset;
arrowheads.attr("transform",function(d){
var l = d - offset;
if (l < 0) {
l = totalLength + l;
} else if (l > totalLength) {
l -= totalLength;
}
var p = pointAtLength(l);
return "translate(" + p + ") rotate( " + angleAtLength(l) + ")";
});
requestAnimationFrame(update);
}
function pointAtLength(l) {
var xy = path.getPointAtLength(l);
return [xy.x, xy.y];
}
// Approximate tangent
function angleAtLength(l) {
var a = pointAtLength(Math.max(l - 0.01,0)), // this could be slightly negative
b = pointAtLength(l + 0.01); // browsers cap at total length
return Math.atan2(b[1] - a[1], b[0] - a[0]) * 180 / Math.PI;
}
</script>
And this is how the <path> looks like without animations:
path {
fill: none;
stroke: #d3008c;
stroke-width: 2px;
}
#arrowhead {
fill: #d3008c;
stroke: none;
}
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="960" height="500">
<path d="M636.5,315c-0.4-18.7,1.9-27.9-5.3-35.9
c-22.7-25-107.3-2.8-118.3,35.9c-7,24.4,20.6,37.2,16,71c-4,29.6-30.8,60.7-56.5,61.1c-30.8,0.4-32.9-43.8-81.7-70.2
c-50.9-27.6-110.1-12.9-125.2-9.2c-66.1,16.4-82.2,56.9-109.2,47.3c-38-13.6-55.9-112.1-19.8-143.5c39-34,121.2,27.7,148.1-3.8
c18-21.1,3.1-74.3-25.2-105.3c-31.1-34.1-70.1-32.4-105.3-76.3c-8.2-10.2-16.9-23.8-15.3-39.7c1.2-11.4,7.5-23.3,15.3-29
c33.8-25,101.6,62.6,193.1,59.5c40.1-1.3,38.7-18.5,99.2-38.9c126.2-42.6,242.4-4.9,297.7,13c54.7,17.7,105.4,35,129.8,82.4
c13,25.3,22.9,67.7,4.6,87c-11.6,12.3-25.1,5.1-46.6,20.6c-2.8,2-28.9,21.4-32.1,49.6c-3.1,27.4,18.7,35,29,70.2
c8.8,30.1,8.5,77.8-18.3,99.2c-32.3,25.8-87,0.6-100-5.3c-69.6-32-67.2-88.4-73.3-109.2z"/>
<defs>
<path id="arrowhead" d="M7,0 L-7,-5 L-7,5 Z" />
</defs>
</svg>
I am creating an editor.
I would like the basic functions on my objects which are rotate/resize and translate.
I've managed to do the three of them but the only problem is now my mouse position doesn't follow the control points (the problem gets even worst for the other control points not shown below)..
You'll find below an example for the right middle resize with a rotation angle of 30 degrees with no mouseY position, note that the mouse follows perfectly my control point when the rotation angle equals 0.
Is there a way easily solve this problem, am I going the wrong way?
Here's the jsfiddle link where you can change the rotate angle in the code to see by yourself JSiddle link. (Just click and drag the black control point to resize the object)
//convert value of range amin to amax to the range bmin to bmax;
function imap(value, amin, amax, bmin, bmax)
{
if ((amax - amin))
return (value - amin) * (bmax - bmin) / (amax - amin) + bmin;
return (0);
};
//get mouse coordinates from the SVG element
function getMouse(el, e)
{
var pt = el.createSVGPoint();
pt.x = e.clientX;
pt.y = e.clientY;
var cursorpt = pt.matrixTransform(el.getScreenCTM().inverse());
return({x: cursorpt.x, y: cursorpt.y})
};
var controlPoint = document.getElementById("c"); //My control point element
var mouseX;
var mouseXClicked = 0;
var scaleX = 1;
var scaleY = 1;
var scaleXClicked = 1;
var control = false; // sets if resizeRightMiddle() should be executed
var rectWidth = 100; //is normally tooken with a getBBox() function
var scale = document.getElementById("scale");
function resizeRightMiddle()
{
//convert difference between original mouse X postion on click and actual X mouse position into a scale factor
plusX = imap(mouseX - mouseXClicked, 0, rectWidth, 0, 1);
//add converted scale factor to the original x scale value
resX = scaleXClicked + plusX;
scale.setAttribute('transform', 'scale(' + resX + ',' + scaleY + ')');
scaleX = resX;
}
var svg = document.getElementById("main");
// save Scale and X mouse coordinate on click
svg.addEventListener("mousedown", function(e){
var coord = getMouse(svg, e);
mouseXClicked = coord.x;
scaleXClicked = scaleX;
});
svg.addEventListener("mousemove", function(e){
//get mouse coordinates
var coord = getMouse(svg, e);
mouseX = coord.x;
// resize if control element has been clicked
if (control)
resizeRightMiddle();
});
// desactivate resize
svg.addEventListener("mouseup", function(e){
control = false;
});
//activate resize
controlPoint.addEventListener("mousedown", function(){
control = true;
});
svg {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
<div>
<svg id="main" width="1000" height="300">
<g transform="translate(66, 56)">
<g id="rotate" transform-origin="center" transform="rotate(30)">
<g id="scale">
<path fill="red" stroke="red" d="M 0 0 L 0 100 L 100 100 L 100 0Z" />
<rect id="c" fill="black" stroke="black" x=95 y=45 width=10 height=10 />
</g>
</g>
</g>
</svg>
</div>
The code below calculates how much the mouse moves in the direction of the rectangle's orientation on each mousemove event, instead of from the beginning of the mousedown to the current mousemove. It then updates updatedRectWidth and uses that to calculate the current desired scale.
var controlPoint = document.getElementById("c");
var control = false;
var origRectWidth = 100;
var scale = document.getElementById("scale");
var relevantMouseMoveDist = 0;
var updatedRectWidth = origRectWidth;
var mouseDownPt = {};
var rotateDiv = document.getElementById("rotate");
var rotateString = rotateDiv.getAttribute('transform');
var rectangleAngle = parseInt(rotateString.slice(rotateString.indexOf("(") + 1)) * Math.PI / 180; // retrieve the angle from the DOM
var relevantMouseMoveDist;
var newMousePosn;
var oldMousePosn;
function resizeRightMiddle()
{
updatedRectWidth += relevantMouseMoveDist;
xScale = updatedRectWidth/origRectWidth;
scale.setAttribute('transform', 'scale(' + xScale + ',1)');
}
var svg = document.getElementById("main");
svg.addEventListener("mousemove", function(e){
if (newMousePosn) {
// the former mouse pos'n
oldMousePosn = {x: newMousePosn.x, y: newMousePosn.y};
// the new mouse pos'n
newMousePosn = {x: e.clientX, y: e.clientY};
// the change in the mouse pos'n coordinates since the last move event
var deltaMouseMove = {
x: newMousePosn.x - oldMousePosn.x,
y: newMousePosn.y - oldMousePosn.y
};
// the dir'n of this movement
var angleOfMouseMovement = Math.atan2(deltaMouseMove.y, deltaMouseMove.x);
// the absolute distance the mouse has moved
var mouseMoveDist = Math.sqrt(
deltaMouseMove.x * deltaMouseMove.x +
deltaMouseMove.y * deltaMouseMove.y
);
// the difference in direction between the mouse movement and orientation of the rectangle
var angleDifference = angleOfMouseMovement - rectangleAngle;
// the portion of the mouse movement that is in the direction of the rectangle's orientation
relevantMouseMoveDist = mouseMoveDist * Math.cos(angleDifference);
// resize the rectangle if necessary
if (control) resizeRightMiddle();
} else {
// establish the mouse pos'n during the first mousemove event
newMousePosn = {x: e.clientX, y: e.clientY};
}
});
svg .addEventListener("mouseup" , function(e){control = false;});
controlPoint.addEventListener("mousedown", function(e){control = true ;});
<div>
<svg id="main" width="1000" height="300">
<g transform="translate(66, 56)">
<g id="rotate" transform-origin="center" transform="rotate(40)">
<g id="scale">
<path fill="red" stroke="red" d="M 0 0 L 0 100 L 100 100 L 100 0Z" />
<rect id="c" fill="black" stroke="black" x=95 y=45 width=10 height=10 />
</g>
</g>
</g>
</svg>
</div>
What I'm trying to do is simple: scale some SVG dots from scale(0) to scale(1) when a sibling element is hovered using vanilla js. They are the red ones in the demo
Here's the basic SVG setup
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg 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 720 576" style="enable-background:new 0 0 720 576;" xml:space="preserve">
<style type="text/css">
.st3 {
fill:red;
}
* {
-webkit-transition:.3s;
transition:.3s;
}
</style>
<g id="Layer_4">
<!-- Shield -->
<path class="st8" d="M601,304.7c-32.4-15.4-68.6-24-106.8-24c-40.4,0-78.5,9.6-112.3,26.6c4.9,79.7,41.9,146.7,109.5,187.6
C559.8,454.1,597,385.6,601,304.7z" />
<path class="st9" d="M420.1,328.7c2.1-4.7,32.5-23.9,72.5-23.9c39.9,0,73.1,20,75.5,24.3c2.4,4.3,5.7,40-12.7,74.6
c-19.7,36.9-53.5,50.1-61.8,50.4c-6.4,0.2-41.8-14.3-62.5-51.6C411.5,367.4,418,333.4,420.1,328.7z" />
<circle class="st10" cx="494.9" cy="373.3" r="35.5" />
</g>
<g id="Layer_8">
<!-- Dots on shield -->
<circle class="st3" cx="578.8" cy="316.2" r="4.6" />
<circle class="st3" cx="543.4" cy="346.2" r="4.6" />
<circle class="st3" cx="505" cy="375.5" r="4.6" />
</g>
</svg>
The issue is that SVG scales based on the origin location, not the current location, thus when a transform is applied it moves the element in addition to scaling it. I am attempting to fix this situation by translating by the BBox() offset, scaling, then translating back but that only seemed to help and not entirely fix the issue.
var shield = document.getElementById("Layer_4"),
dots = document.querySelectorAll("#Layer_8 .st3");
toggleTransform(false);
shield.onmouseover = function () { toggleTransform(true); }
shield.onmouseout = function () { toggleTransform(false); }
function toggleTransform(bool) {
if (!bool) {
for (var i = 0; i < dots.length; i++) {
var box = dots[i].getBBox(),
cx = box.x + box.width / 10,
cy = box.y + box.height / 10;
//dots[i].setAttribute("transform", "translate(" + cx + " " + cy + ") scale(0) translate(" + cx + " " + cy + ")");
dots[i].style.WebkitTransform = "translate(" + cx + "px, " + cy + "px) scale(0) translate(" + -cx + "px, " + -cy + "px)";
}
} else {
for (var i = 0; i < dots.length; i++) {
var box = dots[i].getBBox(),
cx = box.x + box.width / 2,
cy = box.y + box.height / 2;
//dots[i].setAttribute("transform", "translate(0 0) scale(1) translate(0 0)");
dots[i].style.WebkitTransform = "translate(0, 0) scale(1) translate(0, 0)";
}
}
}
I tried using both setAttribute and CSS's transform (I couldn't get setAttribute to transition, presumably because it's not animatable by CSS) but couldn't get it with either. I've only been testing in Chrome
Anyone have an idea how I can scale, while not moving, red dots?
Here's the demo again if you missed it
Edit
I made a function based on RashFlash's answer to make it quite simple to use and also takes into account offsets and different transform origins
function scaleMe(elem, scaleX, scaleY, newOffsetX, newOffsetY, originX, originY) {
newOffsetX = null ? 0 : newOffsetX;
newOffsetY = null ? 0 : newOffsetY;
originX = null ? "center" : originX;
originY = null ? "center" : originY;
var bbox = elem.getBBox(),
cx = bbox.x + (bbox.width / 2),
cy = bbox.y + (bbox.height / 2),
tx = -cx * (scaleX - 1) + newOffsetX,
ty = -cy * (scaleY - 1) + newOffsetY;
if(originX === "left" || originX === "right") {
tx = newOffsetX;
}
if(originY === "top" || originY === "bottom") {
ty = newOffsetY;
}
var scalestr = scaleX + ',' + scaleY,
translatestr = tx + 'px,' + ty + 'px';
elem.style.WebkitTransformOrigin = originX + " " + originY;
elem.style.MozTransformOrigin = originX + " " + originY;
elem.style.msTransformOrigin = originX + " " + originY;
elem.style.transformOrigin = originX + " " + originY;
elem.style.WebkitTransform = "translate(" + translatestr + ") scale(" + scalestr + ")";
elem.style.MozTransform = "translate(" + translatestr + ") scale(" + scalestr + ")";
elem.style.msTransform = "translate(" + translatestr + ") scale(" + scalestr + ")";
elem.style.transform = "translate(" + translatestr + ") scale(" + scalestr + ")";
}
Updated to work with modern browsers that support transform-box
Previously, this approach worked only in Chrome. But spec changes to how transform-origin works, and the addition of transform-box now means that this works in more browsers (currently Chrome, FF, and Opera).
You can actually achieve this effect without JS.
.st3 {
fill: red;
-webkit-transform: scale(1);
-webkit-transform-origin: 50% 50%;
-webkit-transition:.3s;
transform: scale(1);
transform-origin: 50% 50%;
transition:.3s;
transform-box: fill-box;
}
#Layer_4:hover + g .st3 {
-webkit-transform: scale(2);
-webkit-transform-origin: 50% 50%;
-webkit-transition:.3s;
transform: scale(2);
transform-origin: 50% 50%;
transition:.3s;
}
Demo here
if i am not wrong, you want to scale the dots along their center, dots remain their current position and just gets bigger.
if this you want, then following code will help you
var bbox=elementNode.getBBox();
var cx=bbox.x+(bbox.width/2),
cy=bbox.y+(bbox.height/2); // finding center of element
var scalex=1.5, scaley=1.5; // your desired scale
var saclestr=scalex+','+scaley;
var tx=-cx*(scalex-1);
var ty=-cy*(scaley-1);
var translatestr=tx+','+ty;
elementNode.setAttribute('transform','translate('+translatestr+') scale('+saclestr+')');
So what i did, i first translate the dot and than scale it. i use following formula as described in
Transforming Coordinate system
translate(-centerX*(factor-1), -centerY*(factor-1))
scale(factor)
An easier way to do this that does not involve a bunch of geometry is to put the item to be scaled and translated into a parent group ('g').
Then, you apply the translation to the parent group and the scale to the element itself.
var trasnstr = x + ',' + y;
var scalestr = scaleX + ',' + scaleY;
parentElement.setAttribute('transform', 'translate(' + trasnstr + ')');
element.setAttribute('transform', 'scale(' + scalestr + ')');
This will automatically calculate and set transform-origin for any SVG element.
// mainSvgElement is SVG element itself
// svgChildElement is any path, rect, circle etc. inside SVG element
var setTransformOrigin = function(mainSvgElement, svgChildElement) {
var mainRect = mainSvgElement.getBoundingClientRect();
var childRect = svgChildElement.getBoundingClientRect();
var originX = (((childRect.left - mainRect.left) + (childRect.width * 0.5)) / mainRect.width) * 100;
var originY = (((childRect.top - mainRect.top) + (childRect.height * 0.5)) / mainRect.height) * 100;
svgChildElement.style.transformOrigin = originX + "% " + originY + "%";
};
setTransformOrigin(mainSvgElement, svgChildElement);
// set scale now / or you can set in css too
svgChildElement.style.transform = "scale(1.5)";
(function() {
var mainSvgElement = document.querySelector("svg");
var svgChildElement = mainSvgElement.querySelector("path");
// mainSvgElement is SVG element itself
// svgChildElement is any path, rect, circle etc. inside SVG element
var setTransformOrigin = function(mainSvgElement, svgChildElement) {
var mainRect = mainSvgElement.getBoundingClientRect();
var childRect = svgChildElement.getBoundingClientRect();
var originX = (((childRect.left - mainRect.left) + (childRect.width * 0.5)) / mainRect.width) * 100;
var originY = (((childRect.top - mainRect.top) + (childRect.height * 0.5)) / mainRect.height) * 100;
svgChildElement.style.transformOrigin = originX + "% " + originY + "%";
};
setTransformOrigin(mainSvgElement, svgChildElement);
// set scale now / or you can set in css too
svgChildElement.addEventListener("mouseenter", function() {
svgChildElement.style.transform = "scale(1.5)";
});
svgChildElement.addEventListener("mouseleave", function() {
svgChildElement.style.transform = "scale(1)";
});
})();
svg {
width: 100%;
border: 2px solid red;
}
path {
cursor: pointer;
transition: transform 1s;
}
Bring your mouse over on the shape:
<svg class="tb-graph" viewBox="0 0 1006 684" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M574.618 66.3322C575.377 63.1066 573.378 59.87 570.137 59.1784C512.776 46.9376 452.991 52.4821 398.793 75.1494C344.594 97.8167 298.663 136.486 267.096 185.919C265.312 188.711 266.213 192.407 269.042 194.132L357.225 247.895C360.055 249.62 363.738 248.718 365.56 245.95C384.451 217.254 411.493 194.793 443.273 181.502C475.052 168.211 510.032 164.732 543.728 171.435C546.978 172.082 550.207 170.093 550.966 166.867L574.618 66.3322Z" fill="#005453" stroke="#012020" stroke-width="2" />
https://codepen.io/animatedcreativity/pen/qBKXQKZ
So here is what I have:
<path class="..." onmousemove="show_tooltip(event,'very long text
\\\n I would like to linebreak')" onmouseout="hide_tooltip()" d="..."/>
<rect class="tooltip_bg" id="tooltip_bg" ... />
<text class="tooltip" id="tooltip" ...>Tooltip</text>
<script>
<![CDATA[
function show_tooltip(e,text) {
var tt = document.getElementById('tooltip');
var bg = document.getElementById('tooltip_bg');
// set position ...
tt.textContent=text;
bg.setAttribute('width',tt.getBBox().width+10);
bg.setAttribute('height',tt.getBBox().height+6);
// set visibility ...
}
...
Now my very long tooltip text doesn't have a linebreak, even though if I use alert(); it shows me that the text actually DOES have two lines. (It contains a "\" though, how do I remove that one by the way?)
I can't get CDATA to work anywhere.
This is not something that SVG 1.1 supports. SVG 1.2 does have the textArea element, with automatic word wrapping, but it's not implemented in all browsers. SVG 2 does not plan on implementing textArea, but it does have auto-wrapped text.
However, given that you already know where your linebreaks should occur, you can break your text into multiple <tspan>s, each with x="0" and dy="1.4em" to simulate actual lines of text. For example:
<g transform="translate(123 456)"><!-- replace with your target upper left corner coordinates -->
<text x="0" y="0">
<tspan x="0" dy="1.2em">very long text</tspan>
<tspan x="0" dy="1.2em">I would like to linebreak</tspan>
</text>
</g>
Of course, since you want to do that from JavaScript, you'll have to manually create and insert each element into the DOM.
I suppese you alredy managed to solve it, but if someone is looking for similar solution then this worked for me:
g.append('svg:text')
.attr('x', 0)
.attr('y', 30)
.attr('class', 'id')
.append('svg:tspan')
.attr('x', 0)
.attr('dy', 5)
.text(function(d) { return d.name; })
.append('svg:tspan')
.attr('x', 0)
.attr('dy', 20)
.text(function(d) { return d.sname; })
.append('svg:tspan')
.attr('x', 0)
.attr('dy', 20)
.text(function(d) { return d.idcode; })
There are 3 lines separated with linebreak.
With the tspan solution, let's say you don't know in advance where to put your line breaks: you can use this nice function, that I found here:
http://bl.ocks.org/mbostock/7555321
That automatically does line breaks for long text svg for a given width in pixel.
function wrap(text, width) {
text.each(function() {
var text = d3.select(this),
words = text.text().split(/\s+/).reverse(),
word,
line = [],
lineNumber = 0,
lineHeight = 1.1, // ems
y = text.attr("y"),
dy = parseFloat(text.attr("dy")) || 0,
tspan = text.text(null).append("tspan").attr("x", 0).attr("y", y).attr("dy", dy + "em");
while (word = words.pop()) {
line.push(word);
tspan.text(line.join(" "));
if (tspan.node().getComputedTextLength() > width) {
line.pop();
tspan.text(line.join(" "));
line = [word];
tspan = text.append("tspan").attr("x", 0).attr("y", y).attr("dy", ++lineNumber * lineHeight + dy + "em").text(word);
}
}
});
}
use HTML instead of javascript
limitation: the SVG renderer must support HTML rendering
for example, inkscape cannot render such SVG files
<html>
<head><style> * { margin: 0; padding: 0; } </style></head>
<body>
<h1>svg foreignObject to embed html</h1>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 300 300"
x="0" y="0" height="300" width="300"
>
<circle
r="142" cx="150" cy="150"
fill="none" stroke="#000000" stroke-width="2"
/>
<foreignObject
x="50" y="50" width="200" height="200"
>
<div
xmlns="http://www.w3.org/1999/xhtml"
style="
width: 196px; height: 196px;
border: solid 2px #000000;
font-size: 32px;
overflow: auto; /* scroll */
"
>
<p>this is html in svg 1</p>
<p>this is html in svg 2</p>
<p>this is html in svg 3</p>
<p>this is html in svg 4</p>
</div>
</foreignObject>
</svg>
</body></html>
I think this does what you want:
function ShowTooltip(evt, mouseovertext){
// Make tooltip text
var tooltip_text = tt.childNodes.item(1);
var words = mouseovertext.split("\\\n");
var max_length = 0;
for (var i=0; i<3; i++){
tooltip_text.childNodes.item(i).firstChild.data = i<words.length ? words[i] : " ";
length = tooltip_text.childNodes.item(i).getComputedTextLength();
if (length > max_length) {max_length = length;}
}
var x = evt.clientX + 14 + max_length/2;
var y = evt.clientY + 29;
tt.setAttributeNS(null,"transform", "translate(" + x + " " + y + ")")
// Make tooltip background
bg.setAttributeNS(null,"width", max_length+15);
bg.setAttributeNS(null,"height", words.length*15+6);
bg.setAttributeNS(null,"x",evt.clientX+8);
bg.setAttributeNS(null,"y",evt.clientY+14);
// Show everything
tt.setAttributeNS(null,"visibility","visible");
bg.setAttributeNS(null,"visibility","visible");
}
It splits the text on \\\n and for each puts each fragment in a tspan. Then it calculates the size of the box required based on the longest length of text and the number of lines. You will also need to change the tooltip text element to contain three tspans:
<g id="tooltip" visibility="hidden">
<text><tspan>x</tspan><tspan x="0" dy="15">x</tspan><tspan x="0" dy="15">x</tspan></text>
</g>
This assumes that you never have more than three lines. If you want more than three lines you can add more tspans and increase the length of the for loop.
I have adapted a bit the solution by #steco, switching the dependency from d3 to jquery and adding the height of the text element as parameter
function wrap(text, width, height) {
text.each(function(idx,elem) {
var text = $(elem);
text.attr("dy",height);
var words = text.text().split(/\s+/).reverse(),
word,
line = [],
lineNumber = 0,
lineHeight = 1.1, // ems
y = text.attr("y"),
dy = parseFloat( text.attr("dy") ),
tspan = text.text(null).append("tspan").attr("x", 0).attr("y", y).attr("dy", dy + "em");
while (word = words.pop()) {
line.push(word);
tspan.text(line.join(" "));
if (elem.getComputedTextLength() > width) {
line.pop();
tspan.text(line.join(" "));
line = [word];
tspan = text.append("tspan").attr("x", 0).attr("y", y).attr("dy", ++lineNumber * lineHeight + dy + "em").text(word);
}
}
});
}