How to make svg animation with Animated in react-native - svg

ReactNative:
<ScrollView style={styles.container}>
<Svg
height="100"
width="100">
<Circle
cx="50"
cy="50"
r="50"
stroke="blue"
strokeWidth="2.5"
fill="green"/>
</Svg>
</ScrollView>
I want to make Circle scale with Animated.Value. I have tried this :
let AnimatedScrollView = Animated.createAnimatedComponent(ScrollView);
let AnimatedCircle = Animated.createAnimatedComponent(Circle);
<ScrollView style={styles.container}>
<Svg
height="100"
width="100">
<AnimatedCircle
cx="50"
cy="50"
r={this.state.animator}
stroke="blue"
strokeWidth="2.5"
fill="green"/>
</Svg>
</ScrollView>
Then flash back with no error.
How can I do?
update 2016.8.24
I found a new way instead of requestAnimationFrame :
constructor:
this.state = {
animator: new Animated.Value(0),
radius: 1,
};
this.state.animator.addListener((p) => {
this.setState({
radius: p.value,
});
});
render:
<Circle
cx="50"
cy="50"
r={this.state.radius}
stroke="blue"
strokeWidth="2.5"
fill="green"/>
But here the guides gives advice using it sparingly since it might have performance implications in the future.
so what's the best way ?

Use setNativeProps for Much Better Performance.
I did some more tinkering and found a more performant method that makes use of addListener and setNativeProps.
Constructor
constructor(props) {
super(props);
this.state = { circleRadius: new Animated.Value(50) };
this.state.circleRadius.addListener( (circleRadius) => {
this._myCircle.setNativeProps({ r: circleRadius.value.toString() });
});
setTimeout( () => {
Animated.spring( this.state.circleRadius, { toValue: 100, friction: 3 } ).start();
}, 2000)
}
Render
render() {
return(
<Svg height="400" width="400">
<AnimatedCircle ref={ ref => this._myCircle = ref } cx="250" cy="250" r="50" fill="black" />
</Svg>
)
}
Result
And this is what the animation looks like when the 2 second (2000 millisecond) timeout triggers.
So, the main thing you needed to change was using setNativeProps instead of using setState in your listener. This makes a native call and bypasses re-calculating the entire component, which in my case was very complex and slow to do.
Thanks for leading me towards the listener approach!

i have created a simple svg animations library based on a project by another person, for now, only Paths can be animated but more shapes and animations will be added in the future
https://www.npmjs.com/package/react-native-svg-animations

No better answers, and I implemented by adding listener to Animated.Value variable, you can get more info from my question description.

Related

Vue can not attach event listener

I have an svg with some elements in it, the complete code is in here capture event, the aircraft image is positioned via transform attribute in such a way that it falls into the image with href2. The problem is Vue is unable to detect the click event on the aircraft image.
I can't seem to find a way to go around this. I want to be able to attach an event listener to the aircraft image regardless of where on the screen is located.
In jQuery solving these kind of situations is like a breeze of air, but with Vue seems to be a different story.
Here is the HTML
<div id="app">
<svg xmlns="http://www.w3.org/2000/svg" width="1015px" height="580px" viewBox="-50 -50 1015 580" preserveAspectRatio="xMidYMid meet" version="1.1" id="svg">
<g #click="showFlightCard" v-for="(ge, index) in this.gEl" :key="index">
<path :id="index+1" d="M 400 100 L 150 150" stroke="red" stroke-width="3" fill="none" />
<image :id="index" :href="href1" width="48" height="24" transform="translate(251,143)"></image>
</g>
<image x="250" y="10" width="522" height="402.452" id="e4_image" preserveAspectRatio="xMidYMid meet" :href="href2" style="stroke:black;stroke-width:1px;fill:khaki;"/>
</svg>
</div>
Here is the JS, for clarity I avoided the href in here due to 64 bit encoding, which is too long, please look at the jsfiddle which contains the href as well.
var app = new Vue({
el: '#app',
methods: {
showFlightCard: function (e) {
console.log('Click')
}
},
data: {
message: 'Hello Vue!',
gEl: ['A', 'B', 'C'],
href1: 'look at the jsfiddle',
href2: 'look at the jsfiddle'
}
})
You can attach the click listener on the path of the svg itself, or on the parent element of the svg. A click listener on the svg tag itself doesn't seem to fire events.

SVG use tag and ReactJS

So normally to include most of my SVG icons that require simple styling, I do:
<svg>
<use xlink:href="/svg/svg-sprite#my-icon" />
</svg>
Now I have been playing with ReactJS as of late evaluating it as a possible component in my new front-end development stack however I noticed that in its list of supported tags/attributes, neither use or xlink:href are supported.
Is it possible to use svg sprites and load them in this way in ReactJS?
MDN says that xlink:href is deprecated in favor of href. You should be able to use the href attribute directly. The example below includes both versions.
As of React 0.14, xlink:href is available via React as the property xlinkHref. It is mentioned as one of the "notable enhancements" in the release notes for 0.14.
<!-- REACT JSX: -->
<svg>
<use xlinkHref='/svg/svg-sprite#my-icon' />
</svg>
<!-- RENDERS AS: -->
<svg>
<use xlink:href="/svg/svg-sprite#my-icon"></use>
</svg>
Update 2018-06-09: Added info about href vs xlink:href attributes and updated example to include both. Thanks #devuxer
Update 3: At time of writing, React master SVG properties can be found here.
Update 2: It appears that all svg attributes should now be available via react (see merged svg attribute PR).
Update 1: You may want to keep an eye on the svg related issue on GitHub for additional SVG support landing. There are developments in the works.
Demo:
const svgReactElement = (
<svg
viewBox="0 0 1340 667"
width="100"
height="100"
>
<image width="667" height="667" href="https://i.imgur.com/w7GCRPb.png"/>
{ /* Deprecated xlink:href usage */ }
<image width="667" height="667" x="673" xlinkHref="https://i.imgur.com/w7GCRPb.png"/>
</svg>
);
var resultHtml = ReactDOMServer.renderToStaticMarkup(svgReactElement);
document.getElementById('render-result-html').innerHTML = escapeHtml(resultHtml);
ReactDOM.render(svgReactElement, document.getElementById('render-result') );
function escapeHtml(unsafe) { return unsafe.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'"); }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.4.1/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.4.1/umd/react-dom.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.4.1/umd/react-dom-server.browser.development.js"></script>
<h2>Render result of rendering:</h2>
<pre><svg
viewBox="0 0 1340 667"
width="100"
height="100"
>
<image width="667" height="667" href="https://i.imgur.com/w7GCRPb.png"/>
{ /* Deprecated xlink:href usage */ }
<image width="667" height="667" x="673" xlinkHref="https://i.imgur.com/w7GCRPb.png"/>
</svg></pre>
<h2><code>ReactDOMServer.renderToStaticMarkup()</code> output:</h2>
<pre id="render-result-html"></pre>
<h2><code>ReactDOM.render()</code> output:</h2>
<div id="render-result"></div>
Update september 2018: this solution is deprecated, read Jon’s answer instead.
--
React doesn’t support all SVG tags as you say, there is a list of supported tags here. They are working on wider support, f.ex in this ticket.
A common workaround is to inject HTML instead for non-supported tags, f.ex:
render: function() {
var useTag = '<use xlink:href="/svg/svg-sprite#my-icon" />';
return <svg dangerouslySetInnerHTML={{__html: useTag }} />;
}
If you encounter xlink:href, then you can get the equivalent in ReactJS by removing the colon and camelcasing the added text: xlinkHref.
You'll probably eventually be using other namespace-tags in SVG, like xml:space, etc.. The same rule applies to them (i.e., xml:space becomes xmlSpace).
As already said in Jon Surrell's answer, use-tags are supported now. If you are not using JSX, you can implement it like this:
React.DOM.svg( { className: 'my-svg' },
React.createElement( 'use', { xlinkHref: '/svg/svg-sprite#my-icon' }, '' )
)
I created a little helper that works around this issue: https://www.npmjs.com/package/react-svg-use
first npm i react-svg-use -S then simply
import Icon from 'react-svg-use'
React.createClass({
render() {
return (
<Icon id='car' color='#D71421' />
)
}
})
and this will then generate the following markup
<svg>
<use xlink:href="#car" style="fill:#D71421;"></use>
</svg>
I had problems with showing SVG in Gutenberg block, by referencing it with xlink:href. We used xlinkHref property in react, but after compiling, instead to render as xlink:href it was rendered to xlinkhref, and SVG was not displayed. After a lot of examining, I found out that xlink:href is deprecated (although it worked if we add it in html, or directly in chrome dev tools), and that href should be used instead. So after changing it to href it worked.
"SVG 2 removed the need for the xlink namespace, so instead of xlink:href you should use href." https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href
This is the code I used
SVG file
<svg id="svg-source" style="display: none;" aria-hidden="true" xmlns="http://www.w3.org/2000/svg">
<symbol id="svg-arrow-accordion" viewBox="0 0 15 24" fill="none">
<path id="Path_1662" data-name="Path 1662" d="M15.642,14.142h-3V1.5H0v-3H15.642Z" transform="translate(2 2) rotate(45)" fill="currentColor"></path>
</symbol>
</svg>
React file
<svg xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" width="15" height="24">
<use href="#svg-arrow-accordion"></use>
</svg>
This is svg Component.
const SvgComponent = () => {
return <svg width="0" height="0">
<defs>
<symbol id="test" viewBox="0 0 100 100">
<line x1='0' y1='50' x2='100' y2='50' strokeWidth='8' stroke="#000" />
</symbol>
</defs>
</svg>
}
export default SvgComponent
use component
import SvgComponent from './SvgComponent';
export default function App() {
return (
<>
<SvgComponent/>
<svg>
<use xlinkHref="#test"></use>
</svg>
</>
);
}

Zoom on multiple areas in d3.js

I'm planning to have a geoJSON map inside my svg alongside other svg elements. I would like to be able to zoom (zoom+pan) in the map and keep the map in the same location with a bounding box. I can accomplish this by using a clipPath to keep the map within a rectangular area. The problem is that I also want to enable zooming and panning on my entire svg. If I do d3.select("svg").call(myzoom); this overrides any zoom I applied to my map. How can I apply zoom to both my entire svg and to my map? That is, I want to be able to zoom+pan on my map when my mouse is in the map's bounding box, and when the mouse is outside the bounding box, zoom+pan on the entire svg.
Here's example code: http://bl.ocks.org/nuernber/aeaac0e8edcf7ca93ade.
<svg id="svg" width="640" height="480" xmlns="http://www.w3.org/2000/svg" version="1.1">
<defs>
<clipPath id="rectClip">
<rect x="150" y="25" width="400" height="400" style="stroke: gray; fill: none;"/>
</clipPath>
</defs>
<g id="outer_group">
<circle cx="100" cy="50" r="40" stroke="black" stroke-width="2" fill="red" />
<g id="svg_map" style="clip-path: url(#rectClip);">
</g>
</g>
</svg><br/>
<script type="text/javascript">
var svg = d3.select("#svg_map");
var mapGroup = svg.append("g");
var projection = d3.geo.mercator();
var path = d3.geo.path().projection(projection);
var zoom = d3.behavior.zoom()
.translate(projection.translate())
.scale(projection.scale())
.on("zoom", zoomed);
mapGroup.call(zoom);
var pan = d3.behavior.zoom()
.on("zoom", panned);
d3.select("svg").call(pan);
mapGroup.attr("transform", "translate(200,0) scale(2,2)");
d3.json("ne_110m_admin_0_countries/ne_110m_admin_0_countries.geojson", function(collection) {
mapGroup.selectAll("path").data(collection.features)
.enter().append("path")
.attr("d", path)
.attr("id", function(d) { return d.properties.name.replace(/\s+/g, "")})
.style("fill", "gray").style("stroke", "white").style("stroke-width",1);
}
);
function panned() {
var x = d3.event.translate[0];
var y = d3.event.translate[1];
d3.select("#outer_group").attr("transform", "translate("+x+","+y+") scale(" + d3.event.scale + ")");
}
function zoomed() {
previousScale = d3.event.scale;
projection.translate(d3.event.translate).scale(d3.event.scale);
translationOffset = d3.event.translate;
mapGroup.selectAll("path").attr("d", path);
}
</script>
You need two zoom behaviours for that. The first one would be attached to the SVG and the second one to the map. In the zoom handlers you would have to take care of taking the appropriate action for each.

DOM Exception 9 when inserting SVG element with Ember/Handlebars

I have a problem using Ember/Handlebars with SVG elements:
Controller:
display: function() {
this.set('isDisplayed', true);
}
Template:
<button {{action display}}>Display</button>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
{{#if isDisplayed}}
<text>Foo</text>
{{/if}}
</svg>
When I click on the button, I get this error: Uncaught Error: NotSupportedError: DOM Exception 9 (ember.js:18709: Metamorph#htmlFunc on the createContextualFragment line)
Is Ember/Handlebars correctly handling SVG ? What should I do to make this work ?
[EDIT] A little JSBin to see this in action: http://jsbin.com/onejec/2/edit
Handling SVG is somewhat tricky if you want to rely on ember to render DOM elements for you.
But as a starting point you could consider creating a svg wrapper like this:
App.SVGView = Ember.View.extend({
tagName: 'svg',
attributeBindings: ['height', 'width', 'xmlns', 'version'],
height: 100,
width: 100,
xmlns: 'http://www.w3.org/2000/svg',
version: '1.1',
render: function(buffer) {
return buffer.push('<text x="20" y="20" font-family="sans-serif" font-size="20px">Foo!</text>');
}
});
And then hook into the render method to inject your text tag.
This results in the following HTML markup:
<svg id="svg_view" class="ember-view" height="100" width="100" xmlns="http://www.w3.org/2000/svg" version="1.1">
<text x="20" y="20" font-family="sans-serif" font-size="20px">Foo!</text>
</svg>
Here also your working modified jsbin.
Edit
If you need to re-render the view based on some properties that might change you could add an observer to the correspondent property and call this.rerender() inside that method, something like this:
App.SVGView = Ember.View.extend({
tagName: 'svg',
attributeBindings: ['height', 'width', 'xmlns', 'version'],
height: 100,
width: 100,
xmlns: 'http://www.w3.org/2000/svg',
version: '1.1',
render: function(buffer) {
return buffer.push('<text x="20" y="20" font-family="sans-serif" font-size="20px">Foo!</text>');
},
myProperty: 'This value might change',
myPropertyChanged: function() {
this.rerender();
}.observes('myProperty')
});
Hope it helps.

Pure SVG way to fit text to a box

Box size known. Text string length unknown. Fit text to box without ruining its aspect ratio.
After an evening of googling and reading the SVG spec, I'm pretty sure this isn't possible without JavaScript. The closest I could get was using the textLength and lengthAdjust text attributes, but that stretches the text along one axis only.
<svg width="436" height="180"
style="border:solid 6px"
xmlns="http://www.w3.org/2000/svg">
<text y="50%" textLength="436" lengthAdjust="spacingAndGlyphs">UGLY TEXT</text>
</svg>
I am aware of SVG Scaling Text to fit container and fitting text into the box
I didn't find a way to do it directly without Javascript, but I found a JS quite easy solution, without for loops and without modify the font-size and fits well in all dimensions, that is, the text grows until the limit of the shortest side.
Basically, I use the transform property, calculating the right proportion between the desired size and the current one.
This is the code:
<?xml version="1.0" encoding="UTF-8" ?>
<svg version="1.2" viewBox="0 0 1000 1000" width="1000" height="1000" xmlns="http://www.w3.org/2000/svg" >
<text id="t1" y="50" >MY UGLY TEXT</text>
<script type="application/ecmascript">
var width=500, height=500;
var textNode = document.getElementById("t1");
var bb = textNode.getBBox();
var widthTransform = width / bb.width;
var heightTransform = height / bb.height;
var value = widthTransform < heightTransform ? widthTransform : heightTransform;
textNode.setAttribute("transform", "matrix("+value+", 0, 0, "+value+", 0,0)");
</script>
</svg>
In the previous example the text grows until the width == 500, but if I use a box size of width = 500 and height = 30, then the text grows until height == 30.
first of all: just saw that the answer doesn't precisely address your need - it might still be an option, so here we go:
you are rightly observing that svg doesn't support word-wrapping directly. however, you might benefit from foreignObject elements serving as a wrapper for xhtml fragments where word-wrapping is available.
have a look at this self-contained demo (available online):
<?xml version="1.0" encoding="utf-8"?>
<!-- SO: http://stackoverflow.com/questions/15430189/pure-svg-way-to-fit-text-to-a-box -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xhtml="http://www.w3.org/1999/xhtml"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1"
width="20cm" height="20cm"
viewBox="0 0 500 500"
preserveAspectRatio="xMinYMin"
style="background-color:white; border: solid 1px black;"
>
<title>simulated wrapping in svg</title>
<desc>A foreignObject container</desc>
<!-- Text-Elemente -->
<foreignObject
x="100" y="100" width="200" height="150"
transform="translate(0,0)"
>
<xhtml:div style="display: table; height: 150px; overflow: hidden;">
<xhtml:div style="display: table-cell; vertical-align: middle;">
<xhtml:div style="color:black; text-align:center;">Demo test that is supposed to be word-wrapped somewhere along the line to show that it is indeed possible to simulate ordinary text containers in svg.</xhtml:div>
</xhtml:div>
</xhtml:div>
</foreignObject>
<rect x="100" y="100" width="200" height="150" fill="transparent" stroke="red" stroke-width="3"/>
</svg>
I've developed #Roberto answer, but instead of transforming (scaling) the textNode, we simply:
give it font-size of 1em to begin with
calculate the scale based on getBBox
set the font-size to that scale
(You can also use 1px etc.)
Here's the React HOC that does this:
import React from 'react';
import TextBox from './TextBox';
const AutoFitTextBox = TextBoxComponent =>
class extends React.Component {
constructor(props) {
super(props);
this.svgTextNode = React.createRef();
this.state = { scale: 1 };
}
componentDidMount() {
const { width, height } = this.props;
const textBBox = this.getTextBBox();
const widthScale = width / textBBox.width;
const heightScale = height / textBBox.height;
const scale = Math.min(widthScale, heightScale);
this.setState({ scale });
}
getTextBBox() {
const svgTextNode = this.svgTextNode.current;
return svgTextNode.getBBox();
}
render() {
const { scale } = this.state;
return (
<TextBoxComponent
forwardRef={this.svgTextNode}
fontSize={`${scale}em`}
{...this.props}
/>
);
}
};
export default AutoFitTextBox(TextBox);
This is still an issue in 2022. There is no way to define bounds and get text to scale in a pure scalable vector graphic. Adjusting the font size manually is still the only solution it seems, and the examples given are quite buggy. Has anybody figured out a clean solution that works? Judging by the svg spec it looks like a pure solution doesn't exist.
And to provide some sort of answer myself, this resource is the best I've found, is hacky, but works much more robustly: fitrsvgtext - storybook | fitrsvgtext - GitHub
I don't think its the solution for what you want to do but you can use textLength
with percentage ="100%" for full width.
<svg width="436" height="180"
style="border:solid 6px"
xmlns="http://www.w3.org/2000/svg">
<text x="0%" y="50%" textLength="100%">blabla</text>
</svg>
you can also add text-anchor="middle" and change the x position to center perfectly your text
this will not change the fontsize and you will have weird space letterspacing...
JSFIDDLE DEMO

Resources