Using Framer Motion to animate path to the end - javascript

I've got a "link" icon that I am using to explore SVG animation using Framer.
I'm animating the path from 0 initially, to 1 once animated, which does what I want it to; it animates from the start to the end and then from the end back to the start.
Next, I want to try and see what it would look like if I then animated the icon disappearing from the start to the end. This would work in a similar fashion to how it currently works, except it would make the path disappear from the start rather than the end.
So far, I have this:
export default function App() {
const transition = {
duration: 2,
repeat: Infinity,
repeatType: "reverse",
ease: "easeInOut"
};
return (
<svg
height="256"
width="256"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<motion.path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={transition}
/>
</svg>
);
}
And a CodeSandbox link; https://codesandbox.io/s/boring-burnell-kc6gt
I'm having trouble figuring out what I need to change to get the path to animate out from the start to the end.

You can animate pathOffset on your motion path from 0 to 1 to animate out from the start to the end.
But you'll need some way to switch between the two animations. So you'd need to orchestrate the animations with Animation Controls, or switch the animation prop based on some other event in your code.
Here's an example that animates in as you have it, then animates out with pathOffset when the icon is clicked:
https://codesandbox.io/s/interesting-wu-lbocl?file=/src/App.js

Related

How to fit svg path into svg viewbox in React Native

I am trying to implement barcode scanner viewFinder and I want to use svg icon to make it look nice, but I have a problem with forcing the path element inside the svg to take up the full svg width and height. I am using react native and to generate icon i use SVGR https://react-svgr.com/playground/?native=true&typescript=true in the scan handler I set the dimensions of the svg like so:
const handleBarCodeScanned = ({ type, data, bounds }: BarCodeEvent) => {
if (!bounds) return;
const { origin, size } = bounds;
setX(origin.x);
setY(origin.y);
setWidth(size.width);
setHeight(size.height);
};
and the I ise them inside the svg which looks like so
import * as React from "react";
import Svg, { SvgProps, Path } from "react-native-svg";
export const ViewFinder = (props: SvgProps & { top: number; left: number }) => {
const { width, height, top, left } = props;
return (
<Svg
width={width}
height={height}
style={{
borderColor: "green",
borderWidth: 2,
position: "absolute",
left: 0,
top: 0,
width: "100%",
height: "100%",
}}
fill="none"
stroke="green"
preserveAspectRatio="none"
viewBox={`0 0 ${width} ${height}`}
>
<Path d="M6.13 1L6 16a2 2 0 0 0 2 2h15"></Path>
<Path d="M1 6.13L16 6a2 2 0 0 1 2 2v15"></Path>
</Svg>
);
};
original icon is a featerIcons crop icon https://feathericons.com/ and the original code of the icon is:
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-crop"><path d="M6.13 1L6 16a2 2 0 0 0 2 2h15"></path><path d="M1 6.13L16 6a2 2 0 0 1 2 2v15"></path></svg>
as you can see I set the border color and borderWidth on the svg itself, and it scales to fit the container so here everything seems to be ok. I have viewBox and preserveAspectRatio set up its just the inner path not scaling with the svg, and it is not just this icon I have tries several and the issue is still this same so there must be something wrong with my understanding of svg.
Thanks a lot for any help.
Normally a viewBox would be 4 fixed numbers, i.e. unrelated to width and height. That should give you the result you want.
Your content doesn't change in size so your viewBox shouldn't change either.

How to get the exact BBox for svg <tspan>

I am trying to figure out why getBBox() for tspan element of a svg does not return the dimension.
To demonstrate this with an example, if I run BBox on both tsp1 and rect1, it returns the correct dimension for rect1 but not for tsp1
var tsp = document.getElementById('tsp1');
var tspBBox = tsp.getBBox();
var rect = document.getElementById('rect1');
var rectBBox = rect.getBBox();
console.log(tspBBox);
console.log(rectBBox);
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 720">
<text class="t1" id="t1" font-size="20" font-family="PT Mono" text-decoration="underline">
<tspan class="tsp1" id="tsp1" x="10.23" y="135.05">Abc ef ghi</tspan>
</text>
<rect class="rect1" id="rect1" x="9.23" y="112.73368530273439" height="31.546314697265625" width="1" fill="orange" />
</svg>
I was expecting BBox to return the exact x and y for tsp1 but it does not.
I don't know why. I need to pass on the exact values to the succeeding class dynamically.
How can javascript return the exact dimension for the tspan element?
There are a number of methods for measuring text, and they are a bit more complex than defining a simple box. This is because with the dx, dy and rotate attributes, each addressable character can be be positioned individually - moved and rotated in every direction. Therefore, it makes more sense to answer the question where a single character is positioned, and where, after completing one sequence, the next character would be positioned.
In your case none of the above attributes are set ( on the <tspan> or <text> element). In this case is is possible to retrieve the start position of the <tspan> with .getStartPositionOfChar(0) and the horizontal width with .getComputedTextLength().* The height according to the font metrics is the same for all characters in the tspan, so it is enough to return one .getExtentOfChar(0) - 0 refers to the first character within the sequence of addressable characters.
As chrwahl pointed out in his answer, the start position refers to the font-specific baseline and normally will not be identical to the top left corner of a bounding box.
*There is a subtle trick here: if the letter-spacing or word-spacing CSS properties were defined, the "length" returned would not only return the width from the start of the first character to the end of the last, but also would add (or subtract) a spacing value that is defined after the end of the string. In other words: despite its name, the method returns the relative horizontal start position of the next character after the string examined.
var tsp = document.getElementById('tsp1');
var tspPos = tsp.getStartPositionOfChar(0);
console.log('start position', tspPos.x, tspPos.y);
console.log('horizontal advance', tsp.getComputedTextLength());
console.log('vertical extent', tsp.getExtentOfChar(0).height);
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 720">
<text class="t1" id="t1" font-size="20" font-family="PT Mono" text-decoration="underline">
<tspan class="tsp1" id="tsp1" x="10.23" y="135.05">Abc ef ghi</tspan>
</text>
</svg>
It is all about the dominant-baseline. So, there is a differences between where the text is placed according to the dominant-baseline and the box that the text takes up. The value text-before-edge will place the text according to the upper left corner of the box.
var tsp = document.getElementById('tsp1');
var tspBBox = tsp.getBBox();
var rect = document.getElementById('rect1');
var rectBBox = rect.getBBox();
console.log('tspBBox', tspBBox.x, tspBBox.y);
console.log('rectBBox', rectBBox.x, rectBBox.y);
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 100 400 200">
<text class="t1" id="t1" font-size="20" font-family="PT Mono"
text-decoration="underline" dominant-baseline="text-before-edge">
<tspan class="tsp1" id="tsp1" x="10.23" y="135.05">Abc ef ghi</tspan>
</text>
<rect class="rect1" id="rect1" x="9.23" y="112.73368530273439" height="31.546314697265625" width="1" fill="orange" />
</svg>

Is it possible to make SVG equal to path?

Helo I have a problem. I am using npm package which provides pack of weather icons. The problem is the SVG box is big and it breaks positioning that is a lot of white space on website, it does not look nice. Is negative margin the way>
import {wiCloudy} from 'weather-icons-react'
const WeatherDataBox = () => {
return (
icon=<WiCloudy size={256} />
)}
<svg stroke="currentColor" fill="currentColor"
stroke-width="0" viewBox="0 0 30 30"
attr="[object Object]" size="256" height="256" width="256">
<path>*long code*</path>
</svg>
I changed viewbox and it reduced empty background
let ICON_SIZE = 256
let VIEWBOX = "5 5 20 20"
<WiCloudy viewBox={VIEWBOX} size={ICON_SIZE} />

Setting D3 svg.transition to go from slow to fast to slow

I have a D3 graph that allows a user to click a button to take them to a specified node. The button looks like this:
<button type="button" class="btn btn-primary" ng-click="ctrl.panGraph(9)">Go to End</button>
This button will take the user from wherever they are in the svg at the time of click, to the x and y coordinates of the last node, with the id of 9. On click this function is called:
function panGraph (nodeId:any) {
svgWidth = parseInt(svg.style("width").replace(/px/, ""), 10);
svgHeight = parseInt(svg.style("height").replace(/px/, ""), 10);
for (var i = 0; i < renderedNodes.length; i++) {
if (nodeID === renderedNodes[i].id) {
ctrl.selectedNode = renderedNodes[i];
var translate = [svgWidth / 2 - renderedNodes[i].x, svgHeight / 2 - renderedNodes[i].y];
var scale = 1;
svg.transition().duration(4000).ease(d3.easeExpInOut).call(zoom.translate(translate).scale(scale).event);
}
}
}
In the above function I have all the rendered nodes that have been rendered on the page, once I find the matching id I use its x and y coordinates to center the specified node in the middle of the svg. That all works fine.
I am trying to use some animations during the time that the graph is translating to the specified node on button click. When the user clicks the button that takes him or her to the specified node, is it possible to animate the transition so that the transition initially starts slow, then speeds up, but then slows down again at the end as it gets close to the specified node? Thanks
UPDATE:
The above code with the "ease" incluided gives me this console error:
angular.js:13550 TypeError: Cannot read property 'indexOf' of undefined
at Object.d3.ease (d3.js:5844)
at Array.d3_transitionPrototype.ease (d3.js:8838)
at zoomOnNode (DiagramComponent.ts:1128)
at DiagramComponent.ts:1072
at Scope.$digest (angular.js:17073)
at Scope.$apply (angular.js:17337)
at HTMLButtonElement.<anonymous> (angular.js:25023)
at HTMLButtonElement.dispatch (jquery.js:4737)
at HTMLButtonElement.elemData.handle (jquery.js:4549)
Here is the v3 equivalent to Gerardo's post regarding v4:
svg.transition().duration(1000).ease("exp-in-out").call(zoom.translate(translate).scale(scale).event);
For a list of all the easing equivalents from v3 to v4 and other changes:
https://github.com/d3/d3/blob/master/CHANGES.md
One (out of several) solution is to use ease with d3.easeExpInOut, or d3.easePolyInOut.exponent(x) with a high exponent (like x=4 or x=5).
See this snippet. Click the circle to see it moving from left to right, starting slow, speeding up and then slowing down again:
d3.select("circle").on("click", function(){
d3.select(this).transition()
.duration(4000)
.ease(d3.easeExpInOut)
.attr("cx", 360)
});
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="400" height="200">
<circle cx="40" cy="100" r="30" fill="teal"></circle>
<line x1="40" x2="40" y1="100" y2="150" stroke="black" stroke-width="1"></line>
<line x1="360" x2="360" y1="100" y2="150" stroke="black" stroke-width="1"></line>
</svg>

Is path animation possible with SVG.js

There are many examples of SVG path animation, both natively
http://jsfiddle.net/FVqDq/
and with Raphael.js
http://jsfiddle.net/d7d3Z/1/
p.animate({path:"M140 100 L190 60"}, 2000, function() {
r.animate({path:"M190 60 L 210 90"}, 2000);
});
How is this possible with the svg.js library?
No, this is not yet possible with svg.js. I have been looking into it and it will be a rather large implementation. As I try to keep the library small it will never be part of the library itself, but I might write a plugin. Although at the moment I do not have much time on my hands so all help will be appreciated.
UPDATE:
This is now possible with SVG.js out of the box if you use paths with equal commands but different values.
But we also have a path morphing plugin for SVG.js which is probably the thing you are looking for.
There is a quick and dirty way to animate a line with svg.js:
http://jsfiddle.net/c4FSF/1/
draw
.line(0, 0, 0, 0)
.stroke({color: '#000', width: 2})
.animate(1000, SVG.easing.bounce) // Using svg.easing.js plugin(not required)
.during(function(t, morph) {
this.attr({x2:morph(0, 100), y2: morph(0, 100)})
})
Animating complex SVG paths as wout said will require a plugin.
Unfortunately I don't (yet) know enough about SVG, but I'm thinking of writing a plugin which would use the SMIL animation tag. Which is what is used in the first link of the question.
We can make path animation by finding the bounding box of your path and the do like this.
if your path having some clipping -rectangle means like that below
<g id="container_svg_SeriesGroup_0" transform="translate(128.8,435)" clip-path="url(#container_svg_SeriesGroup_0_ClipRect)"><path id="container_svg_John_0" fill="none" stroke-dasharray="5,5" stroke-width="3" stroke="url(#container_svg_John0Gradient)" stroke-linecap="butt" stroke-linejoin="round" d="M 0 -17.25 L 21.7 -112.12499999999999 M 21.7 -112.12499999999999 L 43.4 -51.75 M 43.4 -51.75 L 86.8 -25.875 M 86.8 -25.875 L 108.5 -155.25 "/><defs><clipPath id="container_svg_SeriesGroup_0_ClipRect"><rect id="container_svg_SeriesGroup_0_ClipRect" x="0" y="-155.25" width="118.5" height="148" fill="white" stroke-width="1" stroke="transparent" style="display: inline-block; width: 118.5px;"/></clipPath></defs></g>
var box = $("#"+ path.id")[0].getBBox();
create the rectangle based on the box and the set this rectangle as your clip-path in path.
then increase the width of the rectangle step by step in jquery.animate.
doAnimation: function () {
//cliprect is your clipped rectangle path.
$(clipRect).animate(
{ width: 1000},
{
duration: 2000,
step: function (now, fx) {
$(clipRect).attr("width", now);
}
});
},
jquery.animate step function is used to increase the width of your clip-rect step by step.
You can animate paths using the svg.path.js plugin.
See the first examples (using the .drawAnimated method).
Another option, which we've resorted to, is to use textPath and then use a character.
In our case we're using the • entity, but I'm thinking if you create your own typography in .svg, .woff etc, you can have flat shapes of any kind.
So you would use your character as in here:
http://jsfiddle.net/wbx8J/3/
/* create canvas */
var draw = SVG('canvas').size(400,400).viewbox(0, 0, 1000, 1000)
/* create text */
var text = draw.text(function(add) {
add.tspan('•').dy(27)
})
text.font({ size: 80, family: 'Verdana' })
/* add path to text */
text.path('M 100 400 C 200 300 300 200 400 300 C 500 400 600 500 700 400 C 800 300 900 300 900 300')
/* visualise track */
draw.use(text.track).attr({ fill: 'none'/*, 'stroke-width': 1, stroke: '#f09'*/ })
/* move text to the end of the path */
function up() {
text.textPath.animate(3000).attr('startOffset', '100%').after(down)
}
/* move text to the beginning of the path */
function down() {
text.textPath.animate(3000).attr('startOffset', '0%').after(up)
}
/* start animation */
up()

Categories