RaphaelJS - standardize svg path shape sizes - javascript
I'm building a site that has an svg map of the United States using the very easy usmap jquery plugin which uses Raphael. I have a click event that fires if you click an individual state.
On that occasion, I go to the same US States object to render a single state. The problem is that the paths for each state in the list are relative to each other as rendered in the entire US map of states.
But my requirements are that I need to render each state about the same size as each other when I'm viewing a single state.
I know I can put a multiplier on the path parameters and the overall state will increase/decrease, but I'm looking for a method to determine this multiplier dynamically.
I also tried to use setViewBox, hoping it would increase/decrease the <path> to fill the area. But it didn't work, as far as I could tell.
So, either I need to make the <path> fill the space or I need to figure out a multiplier to apply to scale the <path>
// state coords pulled from All states svg file
var statePolygon = "M 93.573239,6.3617734 L 97.938071,7.8167177 L 107.6377,10.564946 L 116.2057,12.504871 L 136.2516,18.162988 L 159.20739,23.821104 L 174.36801,27.215777 L 173.36373,31.099829 L 169.27051,44.909503 L 164.81238,65.714155 L 161.63584,81.854036 L 161.28429,91.232806 L 148.10315,87.33877 L 132.53264,83.955591 L 118.86585,84.551329 L 117.28528,83.01913 L 111.95881,84.916253 L 107.9821,84.665645 L 105.2606,82.904814 L 103.68223,83.430208 L 99.476903,83.201576 L 97.601755,81.829846 L 92.824862,80.093194 L 91.382778,79.886558 L 86.397035,78.560984 L 84.614222,80.069004 L 78.922841,79.726077 L 74.101997,75.931831 L 74.30643,75.131651 L 74.374575,67.197996 L 72.248826,63.31142 L 68.133618,62.57938 L 67.768708,60.225014 L 65.2543,59.597968 L 62.372763,59.063086 L 60.594498,60.033049 L 58.331251,57.123161 L 58.654572,54.213272 L 61.4028,53.889951 L 63.019405,49.84844 L 60.432837,48.716816 L 60.594498,44.998625 L 64.959331,44.351984 L 62.211103,41.603756 L 60.756158,34.490695 L 61.4028,31.580807 L 61.4028,23.659444 L 59.624535,20.426234 L 61.887782,11.049927 L 63.989368,11.534908 L 66.414275,14.444797 L 69.162503,17.031364 L 72.395712,18.97129 L 76.922205,21.072876 L 79.993756,21.719518 L 82.903645,23.174462 L 86.298518,24.144425 L 88.561764,23.982765 L 88.561764,21.557857 L 89.855048,20.426234 L 91.956634,19.13295 L 92.279955,20.264574 L 92.603276,22.042839 L 90.340029,22.52782 L 90.016708,24.629406 L 91.794974,26.084351 L 92.926597,28.509258 L 93.573239,30.449183 L 95.028183,30.287523 L 95.189843,28.994239 L 94.219881,27.700955 L 93.734899,24.467746 L 94.543201,22.689481 L 93.89656,21.234537 L 93.89656,18.97129 L 95.674825,15.41476 L 94.543201,12.828192 L 92.118294,7.9783781 L 92.441615,7.1700758 z M 84.116548,12.340738 L 86.137312,12.179078 L 86.622294,13.553197 L 88.158073,11.936582 L 90.502155,11.936582 L 91.310458,13.472361 L 89.774678,15.169801 L 90.42133,15.978114 L 89.693853,17.998875 L 88.319734,18.403021 C 88.319734,18.403021 87.430596,18.483857 87.430596,18.160536 C 87.430596,17.837215 88.885551,15.573958 88.885551,15.573958 L 87.188111,15.008141 L 86.86479,16.463095 L 86.137312,17.109737 L 84.60153,14.84648 z";
var width = 175,
height = 125,
paper = Raphael(document.getElementById("statemap"), 350, 250);
paper.setViewBox(0, 0, width, height, true);
// paper.canvas.setAttribute('preserveAspectRatio', true);
paper.path(statePolygon);
Anyone have any ideas?
Update
So, I figured out (thanks, Jordan) that you can get a "size" dimension by capturing the difference between all of the X coord (can be done with y, if you'd rather.) By calculating the diff between the lowest X and the highest X you can get a size.
If you then figure out what a 'normal' size would be, you can then determine a multiplier to apply to each state as a fraction of the state's size relative to the 'normal' one.
In my example, the diff in Kansas is 128. CT is 29, so CT needs a multiplier of 4.41 to bring it up to the relative size of Kansas. TX is 242, so it needs a 0.53 multiplier to bring it down.
This normalizes the sizes of the states. If you then, using Raphael, need to increase/decrease this, you can use the transform function:
p = paper.path(statePolygonNew);
var scale = 1.5;
p.transform("t" + scale*52 + "," + scale*32 + " s" + scale + "");
Thanks,
Scott
There are a number of different ways of approaching this. Here are two of them.
Match the viewbox to the existing path
You used this method incorrectly when you were first seeking a solution. The viewbox is essentially a mechanism for dynamically scaling the way that coordinates in the SVG are translated into coordinates on a screen. By setting the viewbox to the dimensions of the svg in screen coordinates, you are effectively telling it to do nothing -- that's what it does by default.
Instead, try setting the viewbox to the bounding box of the target path:
var statePathString = "..."; // your path here
var statePath = paper.path( statePathString ).attr( { /* your attributes */ } );
var bbox = statePath.getBBox();
paper.setViewBox( bbox.x, bbox.y, bbox.width, bbox.height, true );
This instantiates the path for the state, calculates its dimensions, and zooms the viewbox in to look at just that range of the SVG's coordinate systems. You may need to add additional logic to preserve aspect ratios and add margin, but this is definitely the easiest approach.
Transform the path to fit your existing viewbox
If you do opt to avoid viewbox manipulation -- there are a few valid reasons -- then you can adjust the path to match the default viewing rectangle using a handful of Raphael's built-in utility functions. Here's the approach I'd use, given existing variables width and height reflecting the dimensions of your paper:
var bbox = Raphael.pathBBox( statePathString ); // Handy function to retrieve a bounding box without instantiating a path
// Calculate larger of the two dimensional quotients
var scale = Math.max( bbox.width / width, bbox.height / height );
// Finally, transform the path such that it is translated to have its origin at 0,0 and scaled in such a way that it will fill the SVG
var transformedPathString = Raphael.transformPath( statePathString,
[ "T", 0 - bbox.x, 0 - bbox.y, /* Shift up and left so upper-left extent corresponds to 0,0 */
"S", 1.0 / scale, 1.0 / scale, 0, 0 ] );
At this point you can do whatever you'd like to do with transformedPathString -- presumably, pass it to paper.path and style it up.
Related
Set center point for svg group with javascript
In a game I am currently making you are in a map which is build from nodes and you are warping to different nodes when you click on them. My current map looks like that (created with two.js it's a svg group where each circle and line is svg path element, ex: <path transform="matrix(1 0 0 1 553 120)" d="M 8 0 C 8 2.02 7.085 4.228 5.656 5.656 C 4.228 7.085 2.02 8 0 8 C -2.021 8 -4.229 7.085 -5.657 5.656 C -7.086 4.228 -8 2.02 -8 0 C -8 -2.021 -7.086 -4.229 -5.657 -5.657 C -4.229 -7.086 -2.021 -8 -0.001 -8 C 2.02 -8 4.228 -7.086 5.656 -5.657 C 7.085 -4.229 8 -2.021 8 0 Z " fill="#003251" stroke="#ebebeb" stroke-width="1" stroke-opacity="1" fill-opacity="1" visibility="visible" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" id="19"></path> And the map looks like that: The marked in red node is indicator for your current position on the zone/map. How can I re-position the svg group so the current player position to be always in the center of the container shifting the whole map to left/right/top/bottom and become like that: Using the following function I am able to change x,y coordinates of the svg group: function shiftMap(id_attr, offsetX, offsetY) { let elem = $(id_attr); if (elem) { let params = ' translate(' + offsetX + ',' + offsetY + ')'; elem.attr('transform', params); } } I also have a function to detect current player position node coordinates, so I tried to do: //where playerPosX = X of the current player node location (red mark) //and playerPosY = Y of the current player node location (red mark) shiftMap("svgMapID", playerPosX, playerPosY); Unfortunately the position is not accurate and map shifts/moves everywhere, goes out of the container bounds, etc. Any help is much appreciated. Thank you! Hint: It may be possible to do via twojs anchor point, but I am not sure how. More info here: https://two.js.org/#two-anchor
You have to calculate bounding box of your svg-group and move the group, considering outer offsets of the group itself and inner offsets relative to your center node. Let's say you want this new random node to be at center of your group, highlighted in red. Then you calculate its X and Y related to the container. Let's say that X = 400, Y = 200. Then you calculate the bounding box of all your map group related to the container. In this example, Y1 is probably going to be 0, where X1 you have to find. Try using group.getBoundingClientRect(shallow), as described in Two.js docs. Let's say that 'X1 = 300', 'Y1 = 0'. Having your container W(width) = 1000 and H(height) = 700, let's calculate your final coordinates: finalX= (W/2) - (X + X1) = 500 - 700 =-200 finalY= (H/2) - (Y + Y1) = 350 - 200 =150 That means that you have to move your map group -200 by X (200 to the left) and 150 by Y (150 to the bottom)
Get length of longest line segment in svg unsig javascript
I want to find a way to get the length of the longest line segment in a svg path. Since a path can jump from one point to another the getTotalLength() command doesn't work. As a simple example look at the following path: <path d=" M 0 0 L 100 0 M 0 10 L 50 10 M 0 20 L 150 20 "> While getTotalLength() outputs 300, I want 150 as my output. Is there a way to accomplish this?
I found a way to solve the problem. It works by splitting the path in multiple segments and measuring the seperately: function maxSectionLength(path){ let segments = path.attributes.d.value.split(/(?=[m,M])/), testPath = document.createElementNS('http://www.w3.org/2000/svg',"path"), max = 0; for(let d of segments){ testPath.setAttributeNS(null, "d", d) max = Math.max(max,testPath.getTotalLength()); } return max; }
You may use this function to calculate the distance between 2 points: function dist(x1,y1, x2,y2) { let dx = x2 - x1; let dy = y2 - y1; return Math.sqrt(dx * dx + dy * dy); } In your case the last line begins at x1=0; y1=20 and ends at x2=150; y2=20 Of course you may also need a way to split the d attribute and extract the values for x1,y1,x2,y2. I hope this helps.
For-loop not working for JavaScript animation
I am trying to write a for-loop to repeat the animation of the 'explode' path when a shape button is clicked but the for loop isn't working/executing and I can't see where its gone wrong. Aim of the for loop: loop the process of animating the path and then reversing the animation back to its original path. I know that the problem is somewhere in the for loop as the explode.animate() method just before the loop works. Note: I am using Raphael.js for this animation var explode = paper.path("M 300,400 l 30,50 l 40,-30 l -10,50 l 40,30 l -50,10 l 10,40 l -60,-40 l -50, 30 l 10,-50 l -60,-10 l 70, -20 z"); explode.attr({'fill':"yellow", 'stroke-width':"4"}); explode.hide(); function explode1() { explode.animate({path:("M 300,400 l 30,50 l 40,-30 l -10,50 l 40,30 l -50,10 l 10,40 l -60,-40 l -50, 30 l 10,-50 l -60,-10 l 70, -20 z"), 'fill':"yellow"}, 100, 'ease-out'); } function explode2() { explode.animate({path:"M 300,380 l 5,70 l 60,-30 l -5,70 l 70,30 l -100,-10 l -50,50 l -30,-50 l -90, 10 l 80,-50 l -60,-20 l 90, 10 z", 'fill':"orange"},100,'ease-in'); } bomb1.click(function() { audio.pause(); audio.currentTime = 0; bomb1.remove(); explode.show(); explode.animate({path:"M 300,380 l 5,70 l 60,-30 l -5,70 l 70,30 l -100,-10 l -50,50 l -30,-50 l -90, 10 l 80,-50 l -60,-20 l 90, 10 z", 'fill':"orange"},100,'ease-in'); for(var i = 0; i < 4; i+=1) { if (i % 2 == 0) { explode1(); } else { explode2(); } } }); I have also tried putting the animate methods directly into the for loop instead of writing them as functions and for loop still doesn't work. bomb1.click(function() { audio.pause(); audio.currentTime = 0; bomb1.remove(); explode.show(); explode.animate({path:"M 300,380 l 5,70 l 60,-30 l -5,70 l 70,30 l -100,-10 l -50,50 l -30,-50 l -90, 10 l 80,-50 l -60,-20 l 90, 10 z", 'fill':"orange"},100,'ease-in'); for(var i = 0; i < 5; i+=1) { if (i % 2 == 0) { explode.animate({path:("M 300,400 l 30,50 l 40,-30 l -10,50 l 40,30 l -50,10 l 10,40 l -60,-40 l -50, 30 l 10,-50 l -60,-10 l 70, -20 z"), 'fill':"yellow"}, 100, 'ease-out'); } else { explode.animate({path:"M 300,380 l 5,70 l 60,-30 l -5,70 l 70,30 l -100,-10 l -50,50 l -30,-50 l -90, 10 l 80,-50 l -60,-20 l 90, 10 z", 'fill':"orange"},100,'ease-in'); } };
The animation is not synchronous. By that I mean the the for loop will not wait for the animation to finish before performing the next step of the loop. They will all get started at the same time. What you need to do is trigger the next animation once the first one is complete. RaphaelJS lets you pass a function to the animate() method that gets called when the animation is finished. See the following question for an example: animating paths with raphael
Slow SVG rendering when setting src
I am having a problem when rendering SVG images to canvas. When I generate the svg markup and add it to the src of a new image then render that image to the canvas I get terrible performance. My app drops from real time to unusable. But if I save the same markup as an svg file and load that as an image it runs perfectly. var svgText = `<?xml version="1.0"?> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="239" height="268" id="testSVG" > <defs> <linearGradient id='grad2262398' x1='0' y1='0' x2='0' y2='1'> <stop offset='0.0' stop-color='#bcbbbb'/> <stop offset='1.0' stop-color='#d2a796'/> </linearGradient> </defs> <path d="M 158.5 9.0 L 225.0 97.5 L 208.5 111.0 L 141.0 23.5 L 158.5 9.0 M 118.5 44.0 L 203.0 114.5 L 189.5 132.0 L 104.0 61.5 L 118.5 44.0 M 85.5 90.0 L 185.5 136.0 L 178.0 155.5 L 76.5 111.0 L 85.5 90.0 M 67.5 141.0 L 174.5 163.0 L 171.0 185.0 L 62.5 163.0 L 67.5 141.0 M 16.0 168.0 L 38.0 168.0 L 38.0 234.0 L 192.0 234.0 L 192.0 168.0 L 215.0 168.0 L 215.0 256.0 L 16.0 256.0 L 16.0 168.0 M 60.0 190.0 L 171.0 190.0 L 171.0 213.0 L 60.0 213.0 L 60.0 190.0 " style="fill:url(#grad2262398);"/> </svg>` var image = new Image(); image.src = svgText; // this runs very slowly //while var image = new Image(); image.src = "svgText.svg"; // this renders as fast as a simillar sized bitmap image // yet the content is exactly the same. Above is just an example of how the SVG is formated, the problem does not become an issue until the points in the path get to the several thousand. I can render a SVG with 100K+ points in real-time when loaded from a file, as a Javascript string I get less then 2 frames a second. How can I fix this and still have the auto generated path content available so I can modify it when needed.
RaphaelJS HTML5 Library pathIntersection() bug or alternative optimisation (screenshots)
I have a chart generated using RaphaelJS library. It is just on long path: M 50 122 L 63.230769230769226 130 L 76.46153846153845 130 L 89.6923076923077 128 L 102.92307692307692 56 L 116.15384615384615 106 L 129.3846153846154 88 L 142.6153846153846 114 L 155.84615384615384 52 L 169.07692307692307 30 L 182.3076923076923 62 L 195.53846153846152 130 L 208.76923076923077 74 L 222 130 L 235.23076923076923 66 L 248.46153846153845 102 L 261.6923076923077 32 L 274.9230769230769 130 L 288.15384615384613 130 L 301.38461538461536 32 L 314.6153846153846 86 L 327.8461538461538 130 L 341.07692307692304 70 L 354.30769230769226 130 L 367.53846153846155 102 L 380.7692307692308 120 L 394 112 L 407.2307692307692 68 L 420.46153846153845 48 L 433.6923076923077 92 L 446.9230769230769 128 L 460.15384615384613 110 L 473.38461538461536 78 L 486.6153846153846 130 L 499.8461538461538 56 L 513.0769230769231 116 L 526.3076923076923 80 L 539.5384615384614 58 L 552.7692307692307 40 L 566 130 L 579.2307692307692 94 L 592.4615384615385 64 L 605.6923076923076 122 L 618.9230769230769 98 L 632.1538461538461 120 L 645.3846153846154 70 L 658.6153846153845 82 L 671.8461538461538 76 L 685.0769230769231 124 L 698.3076923076923 110 L 711.5384615384615 94 L 724.7692307692307 130 L 738 130 L 751.2307692307692 66 L 764.4615384615385 118 L 777.6923076923076 70 L 790.9230769230769 130 L 804.1538461538461 44 L 817.3846153846154 130 L 830.6153846153845 36 L 843.8461538461538 92 L 857.076923076923 130 L 870.3076923076923 76 L 883.5384615384614 130 L 896.7692307692307 60 L 910 88 Also below these chart I have a jqueryUI slider of the same width (860px) and centered with the chart. I want when I move the slider to move a dot on the chart accordingly with the slider position. See attached screenshot: As you can see it seems to work fine. I've implemented this behaviour using the pathIntersection() method. On the slide event at each ui.value (x coordinate) I intersect my chartPath (the one from above) with a vertical straight line at the x coordinate. But still there are some problems. One of them is that it runs very hard, and it kinda freezes sometimes.. and very weird sometimes it doesn't seem to intersect at all even it should.. I'll example below 2 cases I identified: M 499.8461538461538 0 L 499.8461538461538 140 M 910 0 L 910 140 Could you please explain why this intersect behaviour happens (it should return a dot).. and the worst part it seems like it happens randomly.. if I use another chartdata. Also if you can identify another (better) solution to syncronise the slider position with the dot on the chart.. would be perfect. I thought about using Element.getPointAtLength(length), but I don't know how. I think I should save the pathSegments and for each to compute the start Length and the finish Length.
I think it would be better to use the source data to find the intersection point manually. Shouldn't be too hard as the points are already sorted by x. Path intersection appears to fail when you check the intersection at one of the vertices, which can be considered a bug but is not really surprising, since the library was never intended for computation geometry in the first place. By way of quick hack you could also try slightly modifying the x value when pathIntersection fails to produce a single value and checking again, e.g. try 499.8461538461538 + 0.0001 * (2 * Math.random() - 1) instead of just 499.8461538461538. You might need to try several times before you get a proper answer, but here it is unlikely. I believe the technique is generally known as perturbation.