Trying to zoom to the cursor's position. I'm translating the image as I zoom (scale), but haven't got things working properly.
If you try the demo, you'll not that if you zoom to one location, the move the mouse and zoom to another location, the circle jumps.
jsFiddle: http://jsfiddle.net/nadirabid/f06n05ep/9/
function setCTM(element, matrix) {
var m = matrix;
var s = "matrix(" + m.a + "," + m.b + "," + m.c + "," + m.d + "," + m.e + "," + m.f + ")";
element.setAttributeNS(null, "transform", s);
}
var svgEl = document.getElementById('svg');
var zoomEl = document.getElementById('zoom');
var zoomScale = 1;
svgEl.addEventListener('wheel', function(e) {
var delta = e.wheelDeltaY;
zoomScale = Math.max(0.5, zoomScale - (delta / 2200));
zoomScale = Math.min(zoomScale, 1.5);
var p = svgEl.createSVGPoint();
p.x = e.clientX;
p.y = e.clientY;
p = p.matrixTransform(svgEl.getCTM().inverse());
var zoomMat = svgEl.createSVGMatrix()
.translate(p.x, p.y)
.scale(zoomScale)
.translate(-p.x, -p.y);
setCTM(zoomEl, zoomMat);
});
svg {
background-color: #eee;
}
body {
overflow: hidden;
}
<svg id="svg" width="400" height="400">
<g id="zoom">
<circle r="40" cx="200" cy="200"></circle>
</g>
</svg>
This is happening because you are replacing the old matrix completely without taking into account the previous "zoom history".
What you should be doing is reading (or remembering) the old matrix and then apply the new translate and scale operations to that.
Related
I have a svg path with a textPath connecting 2 divs from center like this:
item1.style.top="20px";
item1.style.left="20px";
item2.style.top="40px";
item2.style.left="160px";
var x1=parseFloat(item1.style.left)+ item1.offsetWidth / 2;
var y1=parseFloat(item1.style.top) + item1.offsetHeight / 2;
var x2=parseFloat(item2.style.left) + item2.offsetWidth / 2;
var y2=parseFloat(item2.style.top) + item2.offsetHeight / 2;
path1.setAttribute("d",`M ${x1} ${y1} L ${x2} ${y2}`)
*{
margin:0;
}
div{
height:2rem;
width:2rem;
position:absolute;
background-color:black;
box-sizing:border-box;
}
<div id="item1"></div>
<div id="item2" style="width:10rem; height:3rem"></div>
<svg id="svg1" style="overflow:visible">
<path id="path1" fill="none" stroke="red" stroke-width="3" />
<text font-size="24" dy="-10" text-anchor="middle">
<textPath href="#path1" fill="green" startOffset="50%">T</textPath>
</text>
</svg>
But as you can see the Text "T" isn't technically centered because of the height & width
so is there a way to shift the text (without changing the path d) into visual center?
like this:
Note
The height, width & position of the divs will change so a more flexible & versatile approach would be better
You could find the intersection points between the rectangles' borders and your textPath.
The final path start and end coordinates would be the calculated intersection points.
item1.style.top = "20px";
item1.style.left = "20px";
item2.style.top = "40px";
item2.style.left = "160px";
let svg = document.querySelector('svg');
let cx1 = parseFloat(item1.style.left) + item1.offsetWidth / 2;
let cy1 = parseFloat(item1.style.top) + item1.offsetHeight / 2;
let cx2 = parseFloat(item2.style.left) + item2.offsetWidth / 2;
let cy2 = parseFloat(item2.style.top) + item2.offsetHeight / 2;
// text path coordinates
let lTextP = [cx1, cy1, cx2, cy2];
renderLine(svg, lTextP)
// rect1: left, right, top, bottom
let x1 = parseFloat(item1.style.left);
let rx1 = x1 + item1.offsetWidth;
let y1 = parseFloat(item1.style.top);
let by1 = y1 + item1.offsetHeight;
// rect2: left, right, top, bottom
let x2 = parseFloat(item2.style.left);
let rx2 = x1 + item2.offsetWidth;
let y2 = parseFloat(item2.style.top);
let by2 = y2 + item2.offsetHeight;
// 1st rect: right border
let l1 = [rx1, y1, rx1, by1];
renderLine(svg, l1)
// 2nd rect: left border
let l2 = [x2, y2, x2, by2];
renderLine(svg, l2)
// find intersections between textpath and rect borders
let intersection1 = getVectorIntersection(l1, lTextP, true);
renderPoint(svg, intersection1, 'orange', '1%');
let intersection2 = getVectorIntersection(l2, lTextP, true);
renderPoint(svg, intersection2, 'orange', '1%');
// shorten text path according to intersections
[cx1, cy1] = intersection1;
[cx2, cy2] = intersection2;
path1.setAttribute("d", `M ${cx1} ${cy1} L ${cx2} ${cy2}`);
/**
* helper: get intersection coordinates
* based on
* source: https://dirask.com/posts/JavaScript-calculate-intersection-point-of-two-lines-for-given-4-points-VjvnAj
*/
function getVectorIntersection(l1, l2, exact = false, decimals = 3) {
let intersection = [];
let dx1 = l1[0] - l1[2];
let dy1 = l1[1] - l1[3];
let dx2 = l2[0] - l2[2];
let dy2 = l2[1] - l2[3];
// down part of intersection point formula
let d = dx1 * dy2 - dy1 * dx2;
if (d === 0) {
console.log('parallel')
return false;
} else {
// upper part of intersection point formula
let u1 = l1[0] * l1[3] - l1[1] * l1[2];
let u4 = l2[0] * l2[3] - l2[1] * l2[2];
// only exact intersections
let isIntersecting = u4 > d;
if (exact && !isIntersecting) {
return false;
}
// intersection point formula
let px = +((u1 * dx2 - dx1 * u4) / d).toFixed(decimals);
let py = +((u1 * dy2 - dy1 * u4) / d).toFixed(decimals);
intersection = [px, py];
}
return intersection;
}
// debug helper: render coordinates as markers
function renderPoint(svg, coords, fill = "red", r = "0.5%") {
if (coords.length) {
let marker =
'<circle cx="' +
coords[0] +
'" cy="' +
coords[1] +
'" r="' +
r +
'" fill="' +
fill +
'" ><title>' +
coords.join(", ") +
"</title></circle>";
svg.insertAdjacentHTML("beforeend", marker);
}
}
// debug helper: render lines
function renderLine(svg, coords, color = "purple", strokeWidth = 1) {
let [x1n, y1n, x2n, y2n] = coords;
let newLine =
'<line x1="' +
x1n +
'" y1="' +
y1n +
'" x2="' +
x2n +
'" y2="' +
y2n +
'" stroke-width="' + strokeWidth + '" stroke="' + color + '" />';
svg.insertAdjacentHTML("beforeend", newLine);
}
*{
margin:0;
}
.item{
height:2rem;
width:2rem;
position:absolute;
z-index:-1;
background-color:black;
box-sizing:border-box;
}
<div class="item" id="item1"></div>
<div class="item" id="item2" style="width:10rem; height:3rem"></div>
<svg id="svg1" style="overflow:visible">
<text font-size="24" dy="-10" text-anchor="middle">
<textPath href="#path1" fill="green" startOffset="50%">T</textPath>
</text>
<path id="path1" fill="none" stroke="red" stroke-width="3" stroke-linecap="square" />
</svg>
An easier alternative might be to draw the text path between the vertical centers like so:
var x1=parseFloat(item1.style.left)+ item1.offsetWidth ;
var y1=parseFloat(item1.style.top) + item1.offsetHeight / 2;
var x2=parseFloat(item2.style.left);
var y2=parseFloat(item2.style.top) + item2.offsetHeight / 2;
item1.style.top="20px";
item1.style.left="20px";
item2.style.top="40px";
item2.style.left="160px";
var x1=parseFloat(item1.style.left)+ item1.offsetWidth ;
var y1=parseFloat(item1.style.top) + item1.offsetHeight / 2;
var x2=parseFloat(item2.style.left);
var y2=parseFloat(item2.style.top) + item2.offsetHeight / 2;
path1.setAttribute("d",`M ${x1} ${y1} L ${x2} ${y2}`)
*{
margin:0;
}
.item{
height:2rem;
width:2rem;
position:absolute;
z-index:-1;
background-color:black;
box-sizing:border-box;
}
<div class="item" id="item1"></div>
<div class="item" id="item2" style="width:10rem; height:3rem"></div>
<svg id="svg1" style="overflow:visible">
<text font-size="24" dy="-10" text-anchor="middle">
<textPath href="#path1" fill="green" startOffset="50%">T</textPath>
</text>
<path id="path1" fill="none" stroke="red" stroke-width="3" stroke-linecap="square" />
</svg>
The solution provided by #herrstrietzel works but I found another solution
I used this solution and calculated the distance required to center and then used dx attribute to simply shift the text by that much
Is there is anyway stop the scribling
normal(chrome) image:
scribled image(firefox):
I am using code to generate drawing path
document.createElementNS(NS, "path");
Here is the JsFiddle : JSFIDDLE
The problem here seems to be a difference between how offsetX and offsetY are implemented between Chrome and Firefox.
In Firefox, when you cross the red line, the offsetX and offsetY values being returned are relative to the top-left of the red line, not the SVG. Which is why your line values are jumping to the top of the screen and left a bit.
I am not sure whether this is a bug in Firefox or not.
In any case, you should not really be using offsetX and offsetY for this purpose. The "correct" approach is normally to get the clientX and clientY coords and translate them to SVG coordinates.
function screenToSVG(clientX, clientY)
{
// Create an SVGPoint for future math
var pt = svg.createSVGPoint();
pt.x = clientX;
pt.y = clientY;
// Apply the inverse of the CTM (SVG Current Transform Matrix) to get
// the equivalent point in SVG coordinate space
return pt.matrixTransform(svg.getScreenCTM().inverse());
}
The advantage of this approach is that it works even if the SVG is not drawn at 1:1 scale. For example if it has a viewBox.
(function() {
var stage = jQuery('#stage');
var isMouseDown = false;
var NS = "http://www.w3.org/2000/svg";
var penCount = 0;
stage.on('mousedown', mouseDown);
stage.on('mousemove', mouseMove);
stage.on('mouseup', mouseUp);
$('#stageClear').on('click',()=>{
$('.shape').remove()
})
function mouseDown(event) {
var pt = screenToSVG(event.clientX, event.clientY);
isMouseDown = true;
var shape = document.createElementNS(NS, "path");
shape.setAttribute("class", "shape");
shape.setAttribute("fill", "transparent");
shape.setAttribute("id", "pen" + penCount);
shape.setAttribute("stroke", "#2795ee");
shape.setAttribute("stroke-width", "4px");
shape.setAttribute("strokeLinecap", "round");
shape.setAttribute("d", "M " + pt.x + " " + pt.y + " ");
shape.setAttribute("pointer-events", "none");
stage.append(shape);
++penCount;
}
function mouseMove(event) {
var pt = screenToSVG(event.clientX, event.clientY);
if (isMouseDown) {
var depth = jQuery('#pen' + (penCount - 1)).attr("d");
jQuery('#pen' + (penCount - 1)).attr("d", depth + "L " + pt.x + " " + pt.y + " ");
}
}
function mouseUp(event) {
isMouseDown = false;
}
function screenToSVG(clientX, clientY)
{
// Create an SVGPoint for future math
var svg = stage[0];
var pt = svg.createSVGPoint();
pt.x = clientX;
pt.y = clientY;
// Apply the inverse of the CTM (SVG Current Transform Matrix) to get
// the equivalent point in SVG coordinate space
return pt.matrixTransform(svg.getScreenCTM().inverse());
}
})();
#stage{
width:100%;
height:610px;
border:1px solid;
}
#stageClear{
cursor:pointer;
}
#stagetext{
user-select:none;
-moz-user-select:none;
-ms-user-select:none;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<svg id="stage" >
<text id="stagetext" x="10" y="10px">draw on the stage using mouse</text>
<line x1="50" y1="300" x2="800" y2="300" style="stroke:rgb(255,0,0);stroke-width:2" />
</svg>
<button id="stageClear"> clear</button>
I have just started learning about SVGs and wanted to create a for loop to draw many circles in my HTML. Can it be done the way I'm trying to do it, or is what I'm trying to do not possible?
<html>
<body>
<h1>My first SVG for loop</h1>
<script type="text/javascript">
var circlex = 50;
var circley = 50;
for (var i = 0; i < 100; i++) {
<svg width="100" height="100">
<circle cx="circlex + 1" cy="circley + 1" r="40" stroke="green" stroke-width="4" fill="yellow" />
</svg>
};
</script>
</body>
</html>
So close yet so far
You can not put html code directly into JavaScript (that would be cool)
The way JavaScript adds new elements is through DOM manipulation.
So let's go trough the code:
First created an empty SVG document with an xmlns (just set xmlns="http://www.w3.org/2000/svg", it works 99% of the time) and we need an ID to select the element.
Get the SVG element in JavaScript: document.getElementById("svg_circles"). Here we use the ID we set on the element to save it to a variable.
In the for loop: create a circle element: var circle = document.createElementNS(NS, "circle"); The namespace NS is found in 1. it's http://www.w3.org/2000/svg. This seems complex but is needed and just something you have to memorize.
Set circle attributes: now to the attributes: I set the position cx and cy with .setAttribute(). You can try to position them differently.
I also set the size r attribute and the fill (the long line of code on the fill is just for fun on my part, it creates random colors)
Now we are done with the circles, but the JavaScript code does not know where to put them. So we tell it: svgCircle.appendChild() sets the element as a child of our SVG document. So: svgCircle.appendChild(circle); where circle is the created SVG element.
document.addEventListener("DOMContentLoaded", function(event) {
var circlex = 0;
var circley = 0;
var svgCircle = document.getElementById("svg_circles");
var NS = "http://www.w3.org/2000/svg";
for (var i = 0; i < 100; i++) {
var circle = document.createElementNS(NS, "circle");
console.log(circle);
circle.setAttribute("cx", circlex + i);
circle.setAttribute("cy", circley + i);
circle.setAttribute("r", 10);
circle.setAttribute("fill", "rgb(" + Math.floor(Math.random() * 255) + "," + Math.floor(Math.random() * 255) + "," + Math.floor(Math.random() * 255) + ")");
svgCircle.appendChild(circle);
};
});
<svg id="svg_circles" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
</svg>
It is little bit complex than your example, here is fiddle where I converted your pseudo code to js code. This approach could be ok if you using some server side rendering e.g. .net mvc and than iterate svg element. But in js you need to create dom elements pass the configuration and than append that to dom. Here is code: https://jsfiddle.net/9c7ro6x3/1/
var circlex = 50;
var circley = 50;
var svg = document.createElementNS("http://www.w3.org/2000/svg", 'svg');
for (var i = 0; i < 100; i++) {
circlex = circlex + 1;
circley = circley + 1;
var circle = document.createElementNS("http://www.w3.org/2000/svg", 'circle');
circle.setAttribute("cx", circlex);
circle.setAttribute("cy", circley);
circle.setAttribute("r", "40");
circle.setAttribute("stroke", "green");
circle.setAttribute("strokeWidth", 4);
circle.setAttribute("fill", "yellow");
svg.appendChild(circle);
};
document.body.appendChild(svg);
Hello the answer to this question with this fiddle is perfect for what I need to do - drag something along a path.
However they used Rapael - I'd like to update things and use snap.svg instead.
Replacing the reference from Raphael to Snap in this answer doesn't work - does anyone have any ideas why?
This is as far as I've got in this codepen - I want to have a 10 point shape - the outer points of which can move a long a path to the centre.
var s = Snap("#svg");
var paths = [], points = [], circles = [], lines = [], l = 0, searchDl = 1;
var dist = function (pt1, pt2) {
var dx = pt1.x - pt2.x;
var dy = pt1.y - pt2.y;
return Math.sqrt(dx * dx + dy * dy);
};
var gradSearch = function (l0, pt, path) {
var totLen = path.getTotalLength();
l0 = l0 + totLen;
var l1 = l0,
dist0 = dist(path.getPointAtLength(l0 % totLen), pt),
dist1,
searchDir;
console.log(dist0);
if (dist(path.getPointAtLength((l0 - searchDl) % totLen), pt) >
dist(path.getPointAtLength((l0 + searchDl) % totLen), pt)) {
searchDir = searchDl;
} else {
searchDir = -searchDl;
}
l1 += searchDir;
dist1 = dist(path.getPointAtLength(l1 % totLen), pt);
console.log(dist1);
while (dist1 < dist0) {
dist0 = dist1;
l1 += searchDir;
dist1 = dist(path.getPointAtLength(l1 % totLen), pt);
}
l1 -= searchDir;
console.log(l1 % totLen);
return (l1 % totLen);
};
var startCircleToPoly = function () {
// storing original coordinates
this.ox = this.attr("cx");
this.oy = this.attr("cy");
this.attr({opacity: 1});
};
var moveCircleToPoly = function (dx, dy) {
var tmpPt = {
x : this.ox + dx,
y : this.oy + dy
};
var path = paths[this.data('int')];
// move will be called with dx and dy
l = gradSearch(l, tmpPt, path);
//console.log(l);
pt = path.getPointAtLength(l);
this.attr({cx: pt.x, cy: pt.y});
};
var endCircleToPoly = function () {
// restoring state
this.attr({opacity: 1});
};
for(var i = 1; i <= 10; i++){
//polygon points
points.push( s.select('#line' + i).attr('x2') + ' ' + s.select('#line' + i).attr('y2') );
paths.push( s.path('M' + s.select('#line' + i).attr('x2') + ' ' + s.select('#line' + i).attr('y2') + ' L' + s.select('#line' + i).attr('x1') + ' ' + s.select('#line' + i).attr('y1')).attr({stroke: "blue"}) );
lines.push(s.select('#line' + i).attr({'stroke' : ''}));
//circles
circles.push( s.circle(
s.select('#line'+i).attr('x2'),
s.select('#line'+i).attr('y2'),5).attr({
fill: "red",
id: "circle"+i,
style: {"cursor" : "pointer"}
}).data({int: i-1}).drag( moveCircleToPoly, startCircleToPoly, endCircleToPoly )
);
}
//add poly
/*var poly = s.polygon(points);
poly.attr({
id:"poly",
fill:"#555555"
});
*/
<svg id="svg" version="1.1" preserveAspectRatio="xMinYMin meet" class="svg-content" viewBox="-10.109 -107.67 400 400">
<line id="line1" fill="none" x1="82.196" y1="-17.513" x2="107.595" y2="-95.686"/>
<line id="line2" fill="none" x1="82.196" y1="-17.513" x2="148.689" y2="-65.827"/>
<line id="line3" fill="none" x1="82.196" y1="-17.513" x2="164.391" y2="-17.513"/>
<line id="line4" fill="none" x1="82.196" y1="-17.513" x2="148.689" y2="30.801"/>
<line id="line5" fill="none" x1="82.196" y1="-17.513" x2="107.595" y2="60.66"/>
<line id="line6" fill="none" x1="82.196" y1="-17.513" x2="56.796" y2="60.66"/>
<line id="line7" fill="none" x1="82.196" y1="-17.513" x2="15.699" y2="30.801"/>
<line id="line8" fill="none" x1="82.196" y1="-17.513" x2="0" y2="-17.513"/>
<line id="line9" fill="none" x1="82.196" y1="-17.513" x2="15.699" y2="-65.827"/>
<line id="line10" fill="none" x1="82.196" y1="-17.513" x2="56.796" y2="-95.686"/>
</svg>
Thanks!
I think your main problem is that some of the floats are being treated like strings in addition rather than numeric ids. I would also use the data() method in the odd place you weren't before.
So I would do things like the following to force numeracy, or you could use parseInt etc
var tmpPt = {
x : +this.data("ox") + +dx,
y : +this.data("oy") + +dy
};
codepen
Added note: You may want to have a think about matrices and transformations as well as your drag is happening on elements that have a different screen transformation, so when you drag them, they move slightly quicker, so you may want to compensate for that.
when i create dynamically svg text circle is working fine in number.but i am using small letter (example:- ffffffiiiisasdasdas) text circle create half but i need full circle and Shape should be react according to content which we are placing ..
please check image following
1.http://tinypic.com/view.php?pic=15fll6b&s=8
2.http://tinypic.com/view.php?pic=fe1181&s=8
following code use
<svg width="845" height="350" viewBox="0 0 845 350" clip-rule="nonzero" >
<g
data-ng-attr-fill="{{addText.color}}"
data-ng-attr-width="{{addText.w}}"
data-ng-attr-height="{{addText.h}}"
data-ng-attr-transform="{{rotate(addText)}}"
strok <defs>
<path stroke-width = "3"
fill="userSpaceOnUse"
data-ng-attr-id="temp-{{addText.id}}"
data-ng-attr-d="{{makeBox1(addText, true)}}" />
</defs>
<text ng-if="addText.text" glyph-orientation-vertical="90" lengthAdjust="spacingAndGlyphs" "
data-ng-attr-text-anchor="{{addText.text.anchor}}"
data-ng-attr-font-family="{{addText.text.font}}"
data-ng-attr-font-style="{{addText.text.italic ? 'italic' : 'none'}}"
data-ng-attr-font-weight="{{addText.text.bold ? 'bold' : 'normal'}}"
data-ng-attr-font-size="{{addText.text.size}}"
data-ng-attr-x="{{arcMid(addText)}}"
letter-spacing="2";
style="text-align:justify"
kerning="8">
<textPath data-ng-xlink-href="#temp-{{addText.id}}" method = "stretch"
writing-mode="lr-tb" clip-rule="nonzero" xlink:href="">
{{addText.text.text}}</textPath>
</text>
<path fill="none" stroke="#EEE" data-ng-attr-d="{{makeBox1(addText,true)}}" />
</g>
</svg>
javascript
$scope.makeBox1 = function makeBox1(item, temp) {
if (item.c == 1) {
var ma = $('#txtsearch').val();
var legth = ma.length;
console.log(legth + "m");
if (item.r == 0) {
item.r = item.h / 2;
}
var x1 = item.x + item.w / 2,
y1 = item.y + (item.h),
x2 = x1 + 1,
r = item.r;
if (temp) {
x1 = 270 + item.w / 2;
y1 = 30 + (item.h);
x2 = x1 + 1;
}
item.r = ((((((legth) * item.text.size * 5) / (legth / 0.9))) * legth * .02) + (legth * 0.09));
if (item.r > 137) {
item.r = 137;
}
x1 = 424.5;
y1 = 293;
r = item.r;
x2 = 425.5;
var y2 = 293
return "M " + x1 + " " + y1 + " " +
"A " + r + " " + r + " 0 1 1 " + x2 + " " + y2;
}
}
It's working for numbers because they always have a fixed width - or almost always. That is because you typically want numbers to align in columns - in invoices for instance.
Your complicated equation for calculating the circle radius is based on the font size. The formula has obviously been tweaked so it works well with the width of numerals in the font you are using. But it won't work with general non-numeric text. Or probably with numerals in a different font.
It is going to be hard to get this to work perfectly in every case, because different browsers may implement their <textPath> elements differently.
The best you can do is to measure the text length, and then calculate the radius from that, by dividing by (2*PI).
You can get the length of the text in a <text> element by calling getComputedTextLength() on the element.
var msg="ffffffiiiisasdasdas";
// Get SVG
var mysvg = document.getElementById("mysvg");
// Get text length
var tmp = document.createElementNS("http://www.w3.org/2000/svg", "text");
tmp.textContent = msg;
mysvg.appendChild(tmp);
var len = tmp.getComputedTextLength();
mysvg.removeChild(tmp);
//alert("len = "+len);
// Make the circle path for the msg to sit on
var x1 = 424.5,
y1 = 293,
x2 = 425.5,
y2 = 293;
var r = len / (2 * Math.PI);
var circ = document.createElementNS("http://www.w3.org/2000/svg", "path");
circ.setAttribute("id", "circ");
circ.setAttribute("d", "M " + x1 + " " + y1 + " " +
"A " + r + " " + r + " 0 1 1 " + x2 + " " + y2);
mysvg.appendChild(circ);
// Make the textPath element
var tp1 = document.createElementNS("http://www.w3.org/2000/svg", "text");
mysvg.appendChild(tp1);
var tp2= document.createElementNS("http://www.w3.org/2000/svg", "textPath");
tp2.setAttributeNS("http://www.w3.org/1999/xlink", "href", "#circ");
tp2.textContent = msg;
tp1.appendChild(tp2);
svg text {
font-family: sans-serif;
font-size: 20px;
}
#circ {
fill: none;
stroke: black;
}
<svg id="mysvg" width="845" height="350" viewBox="0 0 845 350">
</svg>