Im new to SVG and paths on HTML5 and I have some issues with an animation im trying to do:
In the next link its a preview of what im trying to do: http://jsfiddle.net/fxwL68hr/1/
The problem is: only works on Google Chrome and Firefox Developer Edition.
In summary: When I hover the SVG, all the triangles do an animation. However triangles 3 and 4 actually change the paths coords to create a bigger triangle. How I can animate this change of coords in those triangles without using css d: path() so it can work in all (or at least the majority of) browsers.
The next link is a : CodePen with my solution
I'm not very sure you will like it.
In the HTML I'm adding a defs element with the target path for triangles 3 & 4:
<svg id="svg" class="svg_bg" width="50%" viewBox="0 0 100 75">
<defs>
<path id="t3Target" d="M 100 75 L 0 0 L 0 75 Z" />
<path id="t4Target" d="M 100 75 L 100 0 L 0 0 Z" />
</defs>
<path id="triangle1" class="triangle1" d="M 17.5 28.5 L 55 75 L 81 75 Z"></path>
<path id="triangle2" class="triangle2" d="M 36.5 8 L 54.5 75 L 87 75 Z"></path>
<path id="triangle3" class="triangle3" d="M 110 -25 L 38 75 L 77 75 Z"></path>
<path id="triangle4" class="triangle4" d="M 49 75 L 84 75 L 120 41.5 Z"></path>
</svg>
For the triangles 3 & 4 I'm using JavaScript.
let rid = null;
let shapesRy = [];
class Shape{
constructor(path_a,path_b,morphingPath){
this.target = getArgsRy(path_a);
this.vals = getArgsRy(path_b);
this.morphingPath = morphingPath;
this.memory = [];
for(let i=0; i < this.vals.length; i++){
this.memory[i] = [];
this.memory[i][0] = this.target[i].slice();
this.memory[i][1] = this.vals[i].slice();
this.updatePath();
}
}
updateValues() {
for(let i = 0;i < this.memory.length; i++){
let dist_x = this.target[i][1] - this.vals[i][1];
let vel_x = dist_x/10;
this.vals[i][1] += vel_x;
let dist_y = this.target[i][2] - this.vals[i][2];
let vel_y = dist_y/10;
this.vals[i][2] += vel_y;
}
let dist_x = this.target[0][1] - this.vals[0][1];
if (Math.abs(dist_x) < .01) {
if(rid){window.cancelAnimationFrame(rid);
rid = null;
}
}
}
updatePath() {
let d=`M${this.vals[0][1]},${this.vals[0][2]}`;
for(let i = 1;i < this.vals.length -1; i++){
d += `L${this.vals[i][1]},${this.vals[i][2]}`
}
d +="Z";
this.morphingPath.setAttributeNS(null, "d", d);
}
}
shapesRy.push(new Shape(t3Target,triangle3,triangle3));
shapesRy.push(new Shape(t4Target,triangle4,triangle4));
function Frame() {
rid = window.requestAnimationFrame(Frame);
shapesRy.map((s) => {
s.updateValues();
s.updatePath();
})
}
svg.addEventListener(
"mouseover",
function() {
if (rid) {
window.cancelAnimationFrame(rid);
rid = null;
}
shapesRy.map((s) => {
for(let i = 0;i < s.memory.length; i++){
s.memory[i].reverse();
s.target[i] = s.memory[i][1].slice();
}
})
Frame();
},
false
);
svg.addEventListener(
"mouseout",
eAction,
false
);
function eAction(){
{
if (rid) {
window.cancelAnimationFrame(rid);
rid = null;
}
shapesRy.map((s) => {
for(let i = 0;i < s.memory.length; i++){
s.memory[i].reverse();
s.target[i] = s.memory[i][1].slice();
}
})
Frame();
}
}
function getArgsRy(path) {
let d = path.getAttribute("d").replace(/\r?\n|\r/g, ""); //remove breaklines
if (d.charAt(0) == "m") {
d = "M" + d.slice(1);
}
let argsRX = /(?=[a-zA-Z])/;
let args = d.split(argsRX);
let ArgsRy = [];
args.map(arg => {
let argRy = arg
.slice(1)
.replace(/\-/g, " -")
.split(/[ ,]+/);
argRy.map((p, i) => {
if (p == "") {
argRy.splice(i, 1);
}
});
for (let i = 0; i < argRy.length; i++) {
argRy[i] = parseFloat(argRy[i]);
}
argRy.unshift(arg[0]);
ArgsRy.push(argRy);
});
return ArgsRy;
}
This is a blog post where I'm explaining the code: Morphing in SVG - first steps
There is an additional problem with the CSS animation for the triangles 1 & 2 (your CSS) since CSS transforms on SVG elements are extremely buggy.
You may want to read this article: Transforms on SVG Elements
Alternatively you may want to use JavaScript for all 4 triangles.
There always is a finite transform from one triangle to another. The math behind that is non-trivial, but with a bit of fiddling around with the grafical transformation tool for example of Inkscape, you can find it. That way, I got to the following:
triangle 3: matrix(0, 1.92308, -1,0.634615, 75, -120.673)
triangle 4: matrix(0, -2.14286, 2.98507, -2.30277, -123.881, 352.708)
Addendum: I have done the appropriate math now. I've even described it already in the context of another answer: Draw circle svg orthogonal projections. While in that answer, three points were used to describe a square that then underwent an orthogonal projection, the mathematical content is just this: take three separate points (not in one line) as source and another three points (same constraints) as target, and the function generate() quoted in that answer will give you the transform matrix.
The animation has to run between the neutral matrix(1, 0, 0, 1, 0, 0) and the one above as a transform property. See it working in this fiddle.
Downside: according to Can I Use, IE does not support CSS transforms on SVG elements, but that was also true for your first two triangles.
In addition, why don't you use a CSS transition instead of an animation? You go from one state to a second and back. Describe the base state and the hover state and transition: transform 1s ease does the rest.
Related
let path_string='<path transform="matrix(1,0,0,-1,0,600)" stroke-width=".074" stroke-linecap="round" stroke-linejoin="round" fill="none" stroke="#000000" d="M 274.75 301.4 L 275.41 303.36 L 275.41 307.28 L 289.11 293.57 "/>'
Above shows an SVG path code as a string, I want to extract the x and y coordinates of the path and store it in array. The pattern is that all the coordinate numbers are seperated by spaces before and after.
For example, the above code when used as input should output
x_coor_arr=[274.75,275.41,275.41,289.11];
y_coor_arr=[301.4,303.36,307.28,293.57];
How do I solve this efficiently?
You can get the d attribute from the path element and then:
var svgRaw = 'M 274.75 301.4 L 275.41 303.36 L 275.41 307.28 L 289.11 293.57 ';
svgRaw = svgRaw.split(' ');
var coorX = [];
var coorY = [];
svgRaw = svgRaw.filter((item) => !['M', 'L', ''].includes(item));
svgRaw.forEach((item, index = 1) => {
if (index % 2 === 0) {
return coorX.push(item);
}
return coorY.push(item)
});
console.log(coorX, coorY);
path[1].innerHTML
returns
<path d="M 5,10 l0,0 l 15 ,0l0,15l-15,0l0,-15 z" ....
The first 2 digits after M are the x,y coordinates for the starting point of the SVG path.
path[1].innerHTML.substr(10,2)
returns the x cordinate (5) and
path[1].innerHTML.substr(13,2)
returns the correct y coordinate.
The problem is the values may be single or double or triple digit numbers which will break the substr() way of doing it.
Browsers have a parser built in, use it or you'll just spend your life on wheel reinventing and bugfixing.
Note for Chrome, you'll need to use a polyfill
var path = document.getElementById("p");
var item1 = path.pathSegList[0];
alert("first co-ordinate is " + item1.x + ", " + item1.y);
<svg>
<path id="p" d="M 5,10 l0,0 l 15 ,0l0,15l-15,0l0,-15 z"/>
</svg>
getPathData()
You can also use getPathData() method with a polyfill (currently still a draft not natively supported).
let path = document.querySelector('#path');
let pathData = path.getPathData();
let M = pathData[0]; //first M command
let [Mx, My] = M.values;
console.log('M coordinates:', Mx, My);
<svg width="20px" height="20px" viewBox="5 10 15 15">
<path id="path" d="M5 10l0 0l15 0l0 15l-15 0l0-15z" />
</svg>
<script src="https://cdn.jsdelivr.net/npm/path-data-polyfill#latest/path-data-polyfill.min.js"></script>
Normalize path d attribute and split to array
The svg d attribute allows many shorthand notations like
optional space between command letters and values: M1 1, M 1 1
comma, space or minus (only for negative values) as delimiters M -1,-1, M -1 -1, M-1-1
concatenated floating point values like M.5.5, M 0.5 0.5
and others like repeated implicit commands m.5.5 0 0, M 0.5 0.5 l0 0
That's why we need to normalize/sanitize it before splitting
let d = path1.getAttribute('d');
let M = getMCoordinates(d);
console.log(M);
function getMCoordinates(d){
let dArray = d
// remove new lines and tabs
.replace(/[\n\r\t]/g, "")
// replace comma with space
.replace(/,/g, " ")
// add space before minus sign
.replace(/-/g, " -")
// decompose multiple decimal delimiters like 0.5.5 => 0.5 0.5
.replace(/(\.)(\d+)(\.)(\d+)/g, "$1$2 $3$4")
// split multiple zero valuues like 0 05 => 0 0 5
.replace(/( )(0)(\d+)/g, "$1 $2 $3")
// add space between command letter and values
.replace(/([mlcsqtahvz])/gi, "|$1 ")
.trim()
.split(" ")
.filter(Boolean);
let M = {x: +dArray[1], y: +dArray[2]}
return M;
}
<svg viewBox="0 0 100 100">
<!-- what a mess, but perfectly valid ... -->
<path id="path1" d="
m.5.5 0 0 l 15 0l0,15l-15,0 l0,-15 z
"/>
</svg>
Use the pathSegList interface:
var path = document.querySelector('path');
var moveto = path.pathSegList[0]; // always the first movoto command
var x = movoto.x, y = moveto.y
A simple way is to split your string based on its known format, and get your x,y where you expect them:
const path = "M 5,10 l0,0 l 15 ,0l0,15l-15,0l0,-15 z",
splitted = path.split(" ")[1].split(","),
x = splitted[0],
y = splitted[1];
console.log(x,y);
You can also use regex, but it may not be simpler.
I need to know whether an svg-object is adjacent to another one or not. Therefore I need to get its borders.
Those objects can be a rect or a path. This is fairly easy when they are straight, but I don't know how to do this when they're curved or crooked.
An example of what I mean can be found here or a short example here:
<svg id="mysvg" xmlns="http://www.w3.org/2000/svg" viewBox="0 200 512 512">
<path d="m 223.40414,282.21605 35.53211,-3.88909 0,-18.73833 -19.79899,-0.17678 c -5.83251,7.19542 -10.70707,15.0451 -15.73312,22.8042 z" id="pB"></path>
</svg>
If I use the Box corners, I create a virtual straight rectangular, which adjacent objects are not equal to the adjacents of the rendered object.
How can I do that?
The Snap library offers some nice utilities for your problem. Your question does not define what "adjacent" should really mean. Here is a function that sets a threshold for the maximal distance between to paths:
var threshold = 1;
function isAdjacent (id1, id2) {
var s = Snap("#mysvg");
var first = s.select('#' + id1);
var second = s.select('#' + id2);
var len = first.getTotalLength(first);
var p1, p2;
for (var at = 0; at <= len; at += threshold) {
p1 = first.getPointAtLength(at);
if ( Snap.closestPoint(second, p1.x, p1.y).distance <= threshold) {
return true;
}
}
return false;
}
JsFiddle
I am trying to achieve progressive animation for svg line path. I am using path like as M L M L ..... .. do to this problem the animation will not work perfectly.
Here is my code,
var distancePerPoint = 1;
var drawFPS = 100;
var orig = document.querySelector('path'), length, timer;
var a = document.querySelector('svg');
a.addEventListener('mouseover',startDrawingPath,false);
a.addEventListener('mouseout', stopDrawingPath, false);
function startDrawingPath(){
length = 0;
orig.style.stroke = 'green';
timer = setInterval(increaseLength,600/drawFPS);
}
function increaseLength(a){
var pathLength = orig.getTotalLength();
length += distancePerPoint;
orig.setAttribute("stroke-dasharray", length + ' ,2000');
if (length >= pathLength) clearInterval(timer);
}
function stopDrawingPath(){
clearInterval(timer);
orig.style.stroke = '';
}
<svg id="asd" width="706" height="600">
<path stroke="red" stroke-width="2" d="M 0 239.34 L 105.75 299.25 M 105.75 299.25 L 211.5 279.28 M 211.5 279.28 L 317.25 259.31 M 317.25 259.31 L 423 259.46 M 423 259.46 L 528.75 99.55 M 528.75 99.55 L 634.5 199.40000000000003 " />
</svg>
If I add line path as M L L L L mean it will work perfectly what I expected.. but I need this same behavior in M L M L M L...
how to achieve... without using css
Do you mean that you want the subpaths to animate one after the other, instead of all at the same time?
The answer is you can't. The dash pattern begins/resets at the start of each subpath (each move 'M'). There is no way around this other than:
Fixing the path by removing the unnecessary 'M' commands, or
splitting the lines into separate paths, then triggering them one after another.
You're trying to animate the line using the 'stroke-dasharray' property. but that applies to each line primitive separately, and not the whole path. As you can see in the result. Every M component in the path will reset the internal stroke length values in the svg path renderer.
A more straight forward solution would be to animate the path data itself. Or just add path elements in the animation frame.
I'm having an issue with a little plugin I wrote for the Highstock Navigator. It's meant to just style the navigator outside the bounds of what their built in options allow. The plugin looks like this:
(function (H) {
H.wrap(H.Scroller.prototype, 'init', function (proceed) {
proceed.apply(this, Array.prototype.slice.call(arguments, 1));
//console.log("drawing scroller: ", this);
});
H.wrap(H.Scroller.prototype, 'drawHandle', function (proceed) {
//console.log("drawing handle: ", this);
proceed.apply(this, Array.prototype.slice.call(arguments, 1));
H.each(this.handles, function (handle,index) {
var element = handle.element
var offsetX = -8
var centerPoint = 0;
if(index == 0) {
///topleft//// ///topright//// ///middleright//// ///bottomright//// ///bottomleft/// ////middleleft
$(element).html('<path fill="#333333" d="M '+(7+offsetX)+' 0 L '+(15+offsetX)+' 0 L '+(8+offsetX)+' 9.5 L'+(15+offsetX)+' 20 L '+(7+offsetX)+' 20 L'+(0+offsetX)+' 9.5" stroke-width="0"></path>')
// element.innerHTML = '<polygon fill="#333333" points="'+(7+offsetX)+',0 '+(15+offsetX)+',0 '+(8+offsetX)+',9.5 '+(15+offsetX)+',20 '+(7+offsetX)+',20 '+(0+offsetX)+',9.5"/>'
}
else {
offsetX = -14 ///topleft//// ///topright//// ///middleright//// ///bottomright//// ///bottomleft/// ////middleleft
$(element).html('<path fill="#333333" d="M '+(7+offsetX)+' 0 L '+(15+offsetX)+' 0 L '+(22+offsetX)+' 9.5 L '+(15+offsetX)+' 20 L '+(7+offsetX)+' 20 L '+(14+offsetX)+' 9.5" stroke-width="0"></path>')
//element.innerHTML = '<polygon fill="#333333" points="'+(7+offsetX)+',0 '+(15+offsetX)+',0 '+(22+offsetX)+',9.5 '+(15+offsetX)+',20 '+(7+offsetX)+',20 '+(14+offsetX)+',9.5"/>'
}
$(element).bind('mouseover',function() {
$(this).find('path').attr('fill', '#50504e');
})
$(element).bind('mouseout',function() {
$(this).find('path').attr('fill', '#333333');
})
$(element).attr('transform', "translate("+handle.translateX+','+(handle.translateY-2)+')')
//$(element).addClass('custom_scroll_handle');
})
});
}(Highcharts));
This plugin works fine in Chrome, it draws the navigator as a solid box with two arrows for the left and right handles. It looks like this:
However in Firefox, the same svg block fails to render. I've verified that the actual svg looks identical in the code inspector, and there are no apparent style additions in firefox that would cause it not to display. I've also tried copying and pasting the default handle svg into the plugin to see if the issue was with the way the svg coordinates were set up, but even the default handle svg fails when drawn by the plugin in firefox. When i hover over the left or right handle path element in firefox it shows it as being positioned in the upper right corner of the svg stage with a height and width of 0. Here is a picture of the same page in firefox:
Does anyone have any insight as to what might be causing it not to appear?
Ok I found the problem in case anyone else runs into this issue.
The issue was with the way the svg was being appended by the plugin. Because i was using either innerHTML or jquery.html() to append the content Firefox was apparently treating it like an unstyled custom html tag rather than an svg element. Using the parseSVG method with appendChild from this answer jquery's append not working with svg element? fixed the issue in firefox.
Here is the working plugin:
(function (H) {
H.wrap(H.Scroller.prototype, 'init', function (proceed) {
proceed.apply(this, Array.prototype.slice.call(arguments, 1));
//console.log("drawing scroller: ", this);
});
H.wrap(H.Scroller.prototype, 'drawHandle', function (proceed) {
//console.log("drawing handle: ", this);
proceed.apply(this, Array.prototype.slice.call(arguments, 1));
H.each(this.handles, function (handle,index) {
var element = handle.element
var offsetX = -8
var centerPoint = 0;
if(index == 0) {
element.innerHTML = '' ///topleft//// ///topright//// ///middleright//// ///bottomright//// ///bottomleft/// ////middleleft
element.appendChild(parseSVG('<path fill="#333333" d="M '+(7+offsetX)+' 0 L '+(15+offsetX)+' 0 L '+(8+offsetX)+' 10 L'+(15+offsetX)+' 20 L '+(7+offsetX)+' 20 L'+(0+offsetX)+' 10" stroke-width="0"></path>'))
}
else {
element.innerHTML = ''
offsetX = -14 ///topleft//// ///topright//// ///middleright//// ///bottomright//// ///bottomleft/// ////middleleft
element.appendChild(parseSVG('<path fill="#333333" d="M '+(7+offsetX)+' 0 L '+(15+offsetX)+' 0 L '+(22+offsetX)+' 10 L '+(15+offsetX)+' 20 L '+(7+offsetX)+' 20 L '+(14+offsetX)+' 10" stroke-width="0"></path>'))
}
$(element).bind('mouseover',function() {
$(this).find('path').attr('fill', '#50504e');
})
$(element).bind('mouseout',function() {
$(this).find('path').attr('fill', '#333333');
})
$(element).attr('transform', "translate("+handle.translateX+','+(handle.translateY-2)+')')
})
});
}(Highcharts));