animate line of svg based on user provided value - javascript

How should i animate the line of my svg vertically? I have a svg of thermometer. I want to move the needle of thermometer based on the user provided value but i could only show the svg. The initial value is 1 and the needle should be in 1 but it is in about -19, -18. I have also created a webpackbin to show what i have done
Here is the link
https://www.webpackbin.com/bins/-KhGUeUlftWQUNDSxMrC
The code below i have pasted is not complete as i have to remove path code because of body limit by SO.
const normalize = (value, maxValue) => {
const capacity = parseFloat(maxValue);
// needle ranges from 0 to 50 degrees
// It should not overflow
const nValue = (parseFloat(value) / capacity) * 50;
return Math.max(Math.min(nValue, 50), 0);
};
class HelloSvg extends React.Component {
constructor() {
super();
this.state = {value: '1'};
}
componentDidMount() {
const {value} = this.state;
this.animation.setAttribute('from', this.animation.getAttribute('to'));
this.animation.setAttribute('to', 50 - value);
this.animation.beginElement();
}
handleChange(event) {
this.setState({ value: event.target.value });
}
//shouldComponentUpdate() {
//return false;
//}
render() {
const {value} = this.state;
console.log(this.state.value);
return (
<div>
Hello from React
<input type="range" min='1' max='50' onChange={this.handleChange.bind(this)} />
Value is {this.state.value}
<svg width="100%" height="auto" viewBox="50 50 60 800" version="1.1" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient x1="-103.596615%" y1="53.6675027%" x2="174.725349%" y2="53.6675027%" id="linearGradient-1">
<stop stopColor="#000000" offset="0%"></stop>
<stop stopColor="#B6B6B6" offset="50%"></stop>
<stop stopColor="#FFFFFF" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Page-1" stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
<g id="thermometer">
<g id="callibrations" transform="translate(-3.099210, -1.008722)">
<rect id="rect4197" fill="#1ab394" x="0.46429" y="0.99612" width="227.30853" height="817.64752"></rect>
<path d="M113.15625,43.46875 C98.74432,43.49155 89.08034,59.50943 92.09455,72.90625 C92.30532,282.95724 91.65711,451.01733 92.46875,661.0625 C94.96609,678.28747 121.07484,684.93864 131.09375,670.5625 C140.06366,658.7571 134.72639,643.12281 136.1538,629.53125 C135.90071,426.03222 136.67745,264.52501 135.71875,61.03125 C133.78141,50.75755 123.6736,42.80162 113.15625,43.46875 Z" id="path4415" fill="url(#linearGradient-1)"></path>
<rect id="rect3857" fill="white" x="0.46429" y="0.99612" width="227.30853" height="817.64752"></rect>
<path d="M114.1186,51.45093 C121.88695,51.45093 128.14089,57.70487 128.14089,65.47322 L128.14089,698.52196 C128.14089,706.29031 121.88695,712.54425 114.1186,712.54425 C106.35025,712.54425 100.09631,706.29031 100.09631,698.52196 L100.09631,65.47322 C100.09631,57.70487 106.35025,51.45093 114.1186,51.45093 L114.1186,51.45093 Z" id="rect3048" fill="grey"></path>
<rect id="level" fill="blue" opacity="0.79" x="100.089208" y="500" width="28.0599995" height="200">
<animateTransform
attributeName="height"
to="30"
dur="300ms"
repeatCount="0"
fill="freeze"
ref={(animation) => { this.animation = animation; }}
/>
</rect>
<path d="M128.14089,65.47322 L128.14089,698.52196 C128.14089,706.29031 121.88695,712.54425 114.1186,712.54425 C106.35025,712.54425 100.09631,706.29031 100.09631,698.52196 L100.09631,65.47322 C100.09631,57.70487 106.35025,51.45093 114.1186,51.45093 C121.88695,51.45093 128.14089,57.70487 128.14089,65.47322 Z" id="path3887" fill="grey" opacity="0.454616855"></path>
<path d="M162.16938,727.645267 C162.16938,754.183009 140.65635,775.696112 114.118545,775.696112 C87.580845,775.696112 66.06771,754.183009 66.06771,727.645267 C66.06771,701.107514 87.580845,679.594421 114.118545,679.594421 C140.65635,679.594421 162.16938,701.107514 162.16938,727.645267 Z" id="path3046" fill="#056AAE"></path>
<path d="M162.16938,727.645267 C162.16938,754.183009 140.65635,775.696112 114.118545,775.696112 C87.580845,775.696112 66.06771,754.183009 66.06771,727.645267 C66.06771,701.107514 87.580845,679.594421 114.118545,679.594421 C140.65635,679.594421 162.16938,701.107514 162.16938,727.645267 Z" id="path3051" fill="red" opacity="0.47163122"></path>
</g>
<text id="Unit" fontFamily="ArialMT, Arial" fontSize="32" fontWeight="normal" fill="#0E0E0E">
<tspan x="94" y="32">°F</tspan>
</text>
<g id="callibtext" transform="translate(21.000000, 94.000000)" fontSize="24" fontFamily="ArialMT, Arial" fill="#0E0E0E" fontWeight="normal">
<text id="-30">
<tspan x="0" y="581">-30</tspan>
</text>
<text id="50">
<tspan x="8" y="22">50</tspan>
</text>
<text id="0">
<tspan x="21" y="371">0</tspan>
</text>
</g>
</g>
</g>
</svg>
</div>
);
}
}
export default HelloSvg;
The needle that i want to move is the rect in the code so i have tried to apply animation over there but i see no any effect.

Does the following working example help you at all?
var slider = document.getElementById("slider");
var anim = document.getElementById("anim");
slider.addEventListener("change", function(evt) {
var sliderValue = evt.target.value;
var currentTo = anim.getAttribute("to");
anim.setAttribute("from", currentTo);
anim.setAttribute("to", 400 - sliderValue);
anim.beginElement();
});
<svg width="100" height="400">
<line x1="50" y1="400" x2="50" y2="0" stroke="lightgrey" stroke-width="20"/>
<line x1="50" y1="400" x2="50" y2="400" stroke="red" stroke-width="20">
<animate id="anim"
attributeName="y2"
to="400"
dur="1s"
fill="freeze"/>
</line>
</svg>
<input id="slider" type="range" min="0" max="400" step="1" value="0"/>

Related

How to reverse the order of svg elements

I am working with a svg element which is following
<svg class="layer1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 150 150">
<rect class="bg" id="bg" width="150" height="150" fill="#e6e6e6"></rect>
<circle class="circ0" id="circ0" cx="75" cy="75" r="72" fill="none" stroke="blue" stroke-linecap="round" stroke-linejoin="round"></circle>
<circle class="circ1" id="circ1" cx="75" cy="75" r="69" fill="none" stroke="green" stroke-linecap="round" stroke-linejoin="round"></circle>
<circle class="circ2" id="circ2" cx="75" cy="75" r="66" fill="none" stroke="red" stroke-linecap="round" stroke-linejoin="round"></circle>
<script href="index.js"></script>
</svg>
I want to reverse the order of these circles with javascript, which I am currently doing by this way
const svg = document.querySelector("svg");
var x = document.querySelectorAll("[class^='circ']");
var bucket = [];
x.forEach((a, i) => {
bucket.push(a)
});
bucket.reverse();
x.forEach(
(a, i) => a.parentNode.removeChild(a)
);
bucket.forEach(
(a, i) => {
a.setAttribute("class", 'circ' + [i]);
a.setAttribute("id", "circ" + [i]);
svg.appendChild(a);
}
)
<svg class="layer1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 150 150">
<rect class="bg" id="bg" width="150" height="150" fill="#e6e6e6"></rect>
<circle class="circ0" id="circ0" cx="75" cy="75" r="72" fill="none" stroke="blue" stroke-linecap="round" stroke-linejoin="round"></circle>
<circle class="circ1" id="circ1" cx="75" cy="75" r="69" fill="none" stroke="green" stroke-linecap="round" stroke-linejoin="round"></circle>
<circle class="circ2" id="circ2" cx="75" cy="75" r="66" fill="none" stroke="red" stroke-linecap="round" stroke-linejoin="round"></circle>
<script href="index.js"></script>
</svg>
It gives me this
Is there a better way of doing this?
append(child) by itself moves DOM Nodes. So your code can be simplified.
But for complexer SVG you probably want to swap DOM positions, because there could be other Elements in between you don't want to affect.
Hold CTRL key to see what happens with append
Click to see the swapping version,
a matter of processing an Array and swapping the first with the last element.
Note: append was not available in Internet Explorer, that is why you see most posts using appendChild.
Modern browsers have loads more DOM goodies: replaceWith after , before etc.
<svg viewBox="0 0 10 10" style="height:200px">
<style>
text { font-size: 2px }
[y="3"]{ fill:yellow }
.first { stroke: black; stroke-width: 0.5 }
</style>
<rect class="bg" id="bg" width="10" height="10" fill="grey"></rect>
<circle class="first" id="c0" cx="2" cy="5" r="2" fill="red" />
<text x="0" y="3">R</text>
<circle class="second" id="c1" cx="4" cy="5" r="3" fill="green" />
<text x="1" y="3">G</text>
<circle class="last" id="c2" cx="6" cy="5" r="4" fill="blue" />
<text x="2" y="3">B</text>
<text x="1" y="6">Click Me!</text>
</svg>
<script>
let svg = document.querySelector("svg");
function append() {
[...svg.querySelectorAll("circle")]
.reverse().forEach((c, i) => {
c.parentNode.append(c);
c.setAttribute("class", c.id = 'c' + i);
});
}
function swap() {
function swapElements(e1, e2) {
let {id,previousSibling,className:{baseVal:c2}} = e2;
e1.after(e2); // put e2 after e1
e2.id = e1.id; e2.setAttribute("class", e1.getAttribute("class"));
previousSibling.after(e1); // put e1 after where e2 WAS
e1.id = id; e1.setAttribute("class", c2);
}
let circles = [...svg.querySelectorAll("circle")];
while (circles.length) {
let c1 = circles.shift();
if (circles.length) swapElements(c1, circles.pop())
}
}
svg.onclick = (e) => (e.ctrlKey && append()) || swap();
</script>

SVG animate pattern from top left

I got this simple SVG animation that is transforming the pattern from dots to circles on page load.
<svg width="596" height="255">
<pattern id="pattern-circles" x="0" y="0" width="20" height="20" patternUnits="userSpaceOnUse">
<circle cx="6" cy="6" r="3" stroke="red" stroke-width="2" fill="transparent">
<animate attributeName="r" values="0; 5" dur="2s" begin="0s" repeatCount="0" fill="freeze" />
</circle>
</pattern>
<!-- The canvas with our applied pattern -->
<rect x="0" y="0" width="100%" height="100%" fill="url(#pattern-circles)" />
</svg>
I am pretty new to SVG animation and have a hard time figure out how to achieve my goal, in addition to what I already got (dots to circles), to have this animation start/fade-in from the top left dot and end in the bottom right dot - is this even possible to achieve with this SVG pattern setup? Is there a way to isolate the dots, and stagger them in one-by-one?
Here is one idea where I will generate the circles using JS. On each iteration I increment the delay using i/j to create the to top-left to bottom-right animation. The position is trivial, the cx is based on i and cy on j
var d = 20;
var nx = 596/d; /* number of circles in a row */
var ny = 255/d; /* number of circles in a columns */
let svg = document.querySelector("svg");
for(var i=0;i<nx;i++) {
for(var j=0;j<ny;j++) {
svg.insertAdjacentHTML( 'beforeend','<circle cx="'+(6 + d*i)+'" cy="'+(6 + d*j)+'" r="0" stroke="red" stroke-width="2" fill="transparent"><animate attributeName="r" values="0; 5" dur="1s" begin="'+((i+j)/10)+'s" repeatCount="0" fill="freeze" /></circle>');
}
}
<svg width="596" height="255">
</svg>
To have a to-right animation keep only the i (same logic if you want a to-bottom one by keeping only the j)
var d = 20;
var nx = 596/d; /* number of circles in a row */
var ny = 255/d; /* number of circles in a columns */
let svg = document.querySelector("svg");
for(var i=0;i<nx;i++) {
for(var j=0;j<ny;j++) {
svg.insertAdjacentHTML( 'beforeend','<circle cx="'+(6 + d*i)+'" cy="'+(6 + d*j)+'" r="0" stroke="red" stroke-width="2" fill="transparent"><animate attributeName="r" values="0; 5" dur="1s" begin="'+(i/10)+'s" repeatCount="0" fill="freeze" /></circle>');
}
}
<svg width="596" height="255">
</svg>
An infinite animation:
var d = 20;
var nx = 596/d; /* number of circles in a row */
var ny = 255/d; /* number of circles in a columns */
let svg = document.querySelector("svg");
for(var i=0;i<nx;i++) {
for(var j=0;j<ny;j++) {
svg.insertAdjacentHTML( 'beforeend','<circle cx="'+(6 + d*i)+'" cy="'+(6 + d*j)+'" r="0" stroke="red" stroke-width="2" fill="transparent"><animate attributeName="r" values="0; 5;0" dur="2s" begin="'+((i+j)/20)+'s" repeatCount="indefinite" /></circle>');
}
}
<svg width="596" height="255">
</svg>
And why not from the center:
var d = 20;
var nx = 596/d; /* number of circles in a row */
var ny = 255/d; /* number of circles in a columns */
var ic = nx/2;
var jc = ny/2;
let svg = document.querySelector("svg");
for(var i=0;i<nx;i++) {
for(var j=0;j<ny;j++) {
svg.insertAdjacentHTML( 'beforeend','<circle cx="'+(6 + d*i)+'" cy="'+(6 + d*j)+'" r="0" stroke="red" stroke-width="2" fill="transparent"><animate attributeName="r" values="0; 5;0" dur="2s" begin="'
+( Math.sqrt((ic - i)*(ic - i)+(jc - j)*(jc - j))/20)+'s" repeatCount="indefinite" /></circle>');
}
}
<svg width="596" height="255">
</svg>
Add an animated mask with a linearGradient.
<svg width="596" height="255">
<linearGradient id="prog-mask" x1=0% x2="100%" y1="0%" y2="100%">
<stop offset="0%" stop-color="white" stop-opacity="1" />
<stop offset="5%" stop-color="white" stop-opacity="0">
<animate attributeName="offset" values="0; 1" dur="2s" begin="0s" repeatCount="0" fill="freeze" />
<animate attributeName="stop-opacity" values="0; 1" dur="2s" begin="2s" repeatCount="0" fill="freeze" />
</stop>
<stop offset="100%" stop-color="white" stop-opacity="0" />
</linearGradient>
<mask id="prog-render">
<rect x="0" y="0" width="100%" height="100%" fill="url(#prog-mask)"/>
</mask>
<pattern id="pattern-circles" x="0" y="0" width="20" height="20" patternUnits="userSpaceOnUse">
<circle cx="6" cy="6" r="3" stroke="red" stroke-width="2" fill="transparent">
<animate attributeName="r" values="0; 5" dur="2s" begin="0s" repeatCount="0" fill="freeze" />
</circle>
</pattern>
<!-- The canvas with our applied pattern -->
<rect x="0" y="0" width="100%" height="100%" fill="url(#pattern-circles)" mask="url(#prog-render)"/>
</svg>
You can fade things in with linearGradients. Here, the linearGradients disappear revealing the pattern below them.
Because we have two linearGradients the animation would look faster in the middle than at the start and end (we're multiplying two opacity numbers) so I'm using keySplines to make the animation faster in the middle and slower at the start and end to counteract that.
<svg width="596" height="255">
<defs>
<pattern id="pattern-circles" x="0" y="0" width="20" height="20" patternUnits="userSpaceOnUse">
<circle cx="6" cy="6" r="3" stroke="red" stroke-width="2" fill="none">
</circle>
</pattern>
<linearGradient id="g1" x1="100%" y1="0" x2="0" y2="0">
<stop offset="0%" stop-color="white">
<animate attributeName="stop-opacity" values="1; 0" calcMode="spline" keyTimes="0;1" keySplines="0.5 0 0.5 1" dur="2s" begin="0s" repeatCount="0" fill="freeze" />
</stop>
<stop offset="100%" stop-color="white" stop-opacity="0" />
</linearGradient>
<linearGradient id="g2" x1="0" y1="100%" x2="0" y2="0">
<stop offset="0%" stop-color="white" stop-opacity="1">
<animate attributeName="stop-opacity" values="1; 0" calcMode="spline" keyTimes="0;1" keySplines="0.5 0 0.5 1" dur="2s" begin="0s" repeatCount="0" fill="freeze" />
</stop>
<stop offset="100%" stop-color="white" stop-opacity="0" />
</linearGradient>
</defs>
<rect width="100%" height="100%" fill="url(#pattern-circles)" />
<rect width="100%" height="100%" fill="url(#g1)"/>
<rect width="100%" height="100%" fill="url(#g2)"/>
</svg>

Apply on focus css when click on svg element

I have an svg file.
I would that when I click on a station name, the station stay scaled.
For that I used focus method and then in CSS with the selector :focused apply the effect
But that's not working , nothing is happening.
ps: you can ignore forEach loop it's not so important to understand
let stops = document.querySelector("#stops");
// all the g elements in the stops
let gs = stops.querySelectorAll("g");
// for each g in the gs
gs.forEach(g=>{
// the rectangle in this g element
let thisRect = g.querySelector("rect");
// the circle in this g element
let thisCircle = g.querySelector("circle");
// the coords of the circle's center used for the transform-origin
let x = thisCircle.getAttribute("cx");
let y = thisCircle.getAttribute("cy");
// the bounding box of the group
let bb = g.getBBox();
// set the rect's attributes
thisRect.setAttributeNS(null, "x", bb.x);
thisRect.setAttributeNS(null, "y", bb.y);
thisRect.setAttributeNS(null, "width", bb.width);
thisRect.setAttributeNS(null, "height", bb.height);
// set the value for the transform-origin for this group
g.style.transformOrigin = `${x}px ${y}px`;
})
document.getElementById('g3670').focus()
function showmessage() {
alert("heloo");
}
text{
font-family: Lato;
font-size:16px;
}
g * {pointer-events:none;}
g rect{pointer-events:all;}
#stops g{transform: scale(1);cursor: pointer;}
#stops g:hover {
transform: scale(2);
}
#stops g:active {
transform: scale(2)
}
<svg id="Calque_1" data-name="Calque 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 775.43 469.98">
<defs>
<style>.cls-1{fill:none;stroke-width:5px;}.cls-1,.cls-2{stroke:#a15256;}.cls-2{fill:#fff;}.cls-3{isolation:isolate;font-size:42.79px;font-family:ArialMT, Arial;}</style>
</defs>
<title>line</title>
<path id="path7" class="cls-1" d="M202,67.72,329.33,215.86" transform="translate(-200.1 -66.09)" />
<path id="path8" class="cls-1" d="M329.35,215.87,449,355" transform="translate(-200.1 -66.09)" />
<path id="path9" class="cls-1" d="M449,355c41.53,51.11,96.22,63.08,117.9,69.28" transform="translate(-200.1 -66.09)" />
<path id="path10" class="cls-1" d="M566.86,424.29C655.43,460.48,977.38,391.48,973,536" transform="translate(-200.1 -66.09)" />
<g id="stops">
<g id="g3670">
<rect fill="none"/>
<circle class="cls-2" cx="129.24" cy="149.78" r="13.58" />
<text id="text3668" class="cls-3" transform="translate(145 160)">Station1</text>
</g>
<g id="g3700">
<rect fill="none"/>
<circle class="cls-2" cx="248.91" cy="288.93" r="13.58" />
<text id="text3698" class="cls-3" transform="translate(270 300)">Station2</text>
</g>
<g id="g3750">
<rect fill="none"/>
<circle class="cls-2" cx="366.75" cy="358.2" r="13.58" />
<text id="text3748" class="cls-3" transform="translate(200 400)">Station3</text>
</g>
</g>
</svg>
Add tabindex to stations:
let stops = document.querySelector("#stops");
// all the g elements in the stops
let gs = stops.querySelectorAll("g");
// for each g in the gs
gs.forEach((g, i) => {
g.setAttribute('tabindex', i);
// the rectangle in this g element
let thisRect = g.querySelector("rect");
// the circle in this g element
let thisCircle = g.querySelector("circle");
// the coords of the circle's center used for the transform-origin
let x = thisCircle.getAttribute("cx");
let y = thisCircle.getAttribute("cy");
// the bounding box of the group
let bb = g.getBBox();
// set the rect's attributes
thisRect.setAttributeNS(null, "x", bb.x);
thisRect.setAttributeNS(null, "y", bb.y);
thisRect.setAttributeNS(null, "width", bb.width);
thisRect.setAttributeNS(null, "height", bb.height);
// set the value for the transform-origin for this group
g.style.transformOrigin = `${x}px ${y}px`;
})
document.getElementById('g3670').focus()
function showmessage() {
alert("heloo");
}
text{
font-family: Lato;
font-size:16px;
}
g * {pointer-events:none;}
g rect{pointer-events:all;}
#stops g{transform: scale(1);cursor: pointer;transition:.3s}
#stops g:hover {
transform: scale(2);
}
#stops g:focus {
outline: 0;
transform: scale(2)
}
<svg id="Calque_1" data-name="Calque 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 775.43 469.98">
<defs>
<style>.cls-1{fill:none;stroke-width:5px;}.cls-1,.cls-2{stroke:#a15256;}.cls-2{fill:#fff;}.cls-3{isolation:isolate;font-size:42.79px;font-family:ArialMT, Arial;}</style>
</defs>
<title>line</title>
<path id="path7" class="cls-1" d="M202,67.72,329.33,215.86" transform="translate(-200.1 -66.09)" />
<path id="path8" class="cls-1" d="M329.35,215.87,449,355" transform="translate(-200.1 -66.09)" />
<path id="path9" class="cls-1" d="M449,355c41.53,51.11,96.22,63.08,117.9,69.28" transform="translate(-200.1 -66.09)" />
<path id="path10" class="cls-1" d="M566.86,424.29C655.43,460.48,977.38,391.48,973,536" transform="translate(-200.1 -66.09)" />
<g id="stops">
<g id="g3670">
<rect fill="none"/>
<circle class="cls-2" cx="129.24" cy="149.78" r="13.58" />
<text id="text3668" class="cls-3" transform="translate(145 160)">Station1</text>
</g>
<g id="g3700">
<rect fill="none"/>
<circle class="cls-2" cx="248.91" cy="288.93" r="13.58" />
<text id="text3698" class="cls-3" transform="translate(270 300)">Station2</text>
</g>
<g id="g3750">
<rect fill="none"/>
<circle class="cls-2" cx="366.75" cy="358.2" r="13.58" />
<text id="text3748" class="cls-3" transform="translate(200 400)">Station3</text>
</g>
</g>
</svg>
This is how I would do it: on click I would toggle a class active for the clicked group. In your code I've added this:
g.addEventListener("click",()=>{
g.classList.toggle("active")
})
inside the forEach.
let stops = document.querySelector("#stops");
// all the g elements in the stops
let gs = stops.querySelectorAll("g");
// for each g in the gs
gs.forEach(g=>{
// the rectangle in this g element
let thisRect = g.querySelector("rect");
// the circle in this g element
let thisCircle = g.querySelector("circle");
// the coords of the circle's center used for the transform-origin
let x = thisCircle.getAttribute("cx");
let y = thisCircle.getAttribute("cy");
// the bounding box of the group
let bb = g.getBBox();
// set the rect's attributes
thisRect.setAttributeNS(null, "x", bb.x);
thisRect.setAttributeNS(null, "y", bb.y);
thisRect.setAttributeNS(null, "width", bb.width);
thisRect.setAttributeNS(null, "height", bb.height);
// set the value for the transform-origin for this group
g.style.transformOrigin = `${x}px ${y}px`;
g.addEventListener("click",()=>{
g.classList.toggle("active")
})
})
document.getElementById('g3670').focus()
function showmessage() {
console.log("heloo");
}
text{
font-family: Lato;
font-size:16px;
}
g * {pointer-events:none;}
g rect{pointer-events:all;}
#stops g{transform: scale(1);cursor: pointer;}
#stops g:hover {
transform: scale(2);
}
#stops g.active {
transform: scale(2)
}
<svg id="Calque_1" data-name="Calque 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 775.43 469.98">
<defs>
<style>.cls-1{fill:none;stroke-width:5px;}.cls-1,.cls-2{stroke:#a15256;}.cls-2{fill:#fff;}.cls-3{isolation:isolate;font-size:42.79px;font-family:ArialMT, Arial;}</style>
</defs>
<title>line</title>
<path id="path7" class="cls-1" d="M202,67.72,329.33,215.86" transform="translate(-200.1 -66.09)" />
<path id="path8" class="cls-1" d="M329.35,215.87,449,355" transform="translate(-200.1 -66.09)" />
<path id="path9" class="cls-1" d="M449,355c41.53,51.11,96.22,63.08,117.9,69.28" transform="translate(-200.1 -66.09)" />
<path id="path10" class="cls-1" d="M566.86,424.29C655.43,460.48,977.38,391.48,973,536" transform="translate(-200.1 -66.09)" />
<g id="stops">
<g id="g3670">
<rect fill="none"/>
<circle class="cls-2" cx="129.24" cy="149.78" r="13.58" />
<text id="text3668" class="cls-3" transform="translate(145 160)">Station1</text>
</g>
<g id="g3700">
<rect fill="none"/>
<circle class="cls-2" cx="248.91" cy="288.93" r="13.58" />
<text id="text3698" class="cls-3" transform="translate(270 300)">Station2</text>
</g>
<g id="g3750">
<rect fill="none"/>
<circle class="cls-2" cx="366.75" cy="358.2" r="13.58" />
<text id="text3748" class="cls-3" transform="translate(200 400)">Station3</text>
</g>
</g>
</svg>

JavaScript - SVG animation position changed event

I'm trying to make a simple game of pong using SVGs and vanilla JavaScript. The issue that I'm running into is, how can I bind an event to determine when the position of the ball has moved? So for example, is there anything like:
document.getElementById("ball").getAttribute("cx").onChange = ...;
The code that I've currently built is:
window.onload = function() {
}
window.onkeydown = function(e){
/*
Player 1:
up -> 38
down -> 40
*/
var increment = 0;
if (e.keyCode === 38 || e.keyCode === 40) {
increment = 5;
if (e.keyCode === 38) {
increment = -increment;
}
}
document.getElementById("paddle1").setAttribute("y", parseFloat(document.getElementById("paddle1").getAttribute("y")) + increment);
};
window.onmousemove = function(e) {
// Player2: Based on the y position of the mouse
document.getElementById("paddle2").setAttribute("y", e.clientY);
}
<svg width="576px" height="300px">
<!-- background -->
<rect x="0" y="0" width="576" height="300" stroke="black" stroke-width="1" />
<!-- ball -->
<circle cx="0" cy="0" r="5" fill="white" id="ball">
<animateMotion path="M 0 150 H 576 Z" dur="5s" repeatCount="indefinite" />
</circle>
<!-- mid-point -->
<line stroke-dasharray="5, 10" x1="287.5" y1="1" x2="287.5" y2="300" stroke="white" stroke-width="1" />
<!-- scores -->
<text x="261" y="27" font-family="monospace" font-size="22px" fill="white" id="score1">0</text>
<text x="298" y="27" font-family="monospace" font-size="22px" fill="white" id="score2">0</text>
<!-- paddles -->
<rect x="10" y="10" width="5" height="25" stroke="white" stroke-width="1" fill="white" id="paddle1" />
<rect x="561" y="10" width="5" height="25" stroke="white" stroke-width="1" fill="white" id="paddle2" />
</svg>
Please keep in mind that while I have a fairly extensive JavaScript background, this is my first jump into SVGs.
There does not seem to be an event emitted upon a change of an animated value, not even with a coarse granularity (ie. at given percentages of the animation completed).
However, the current animated values can be queried, so calling a pseudo-handler in regular intervals allows to keep track of the animation's progress within reasonable limits.
The following proof-of-concept standalone svg complements and modifies the code from the question:
define an interval handler to write the current x position of the ball to the console
set up the interval handler in the onLoad handler
Redefine the animation using the animate element
<?xml version="1.0" encoding="UTF-8"?>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 1000 500"
width="1150px" height="600px"
>
<script type="text/javascript">
function cb_ball ( pdom_ball ) {
console.log ( `attr 'cx': '${pdom_ball.cx.animVal.value}'.`);
} // cb_ball
window.onload = function() {
let dom_ball = document.getElementById("ball")
;
setInterval ( () => { cb_ball ( dom_ball ); }, 100 );
}
window.onkeydown = function(e){
/*
Player 1:
up -> 38
down -> 40
*/
var increment = 0;
if (e.keyCode === 38 || e.keyCode === 40) {
increment = 5;
if (e.keyCode === 38) {
increment = -increment;
}
}
document.getElementById("paddle1").setAttribute("y", parseFloat(document.getElementById("paddle1").getAttribute("y")) + increment);
};
window.onmousemove = function(e) {
// Player2: Based on the y position of the mouse
document.getElementById("paddle2").setAttribute("y", e.clientY);
}
</script>
<!-- background -->
<rect x="0" y="0" width="576" height="300" stroke="black" stroke-width="1" />
<!-- ball -->
<circle cx="0" cy="150" r="5" fill="white" id="ball">
<!-- was: animateMotion path="M 0 150 H 576 Z" dur="5s" repeatCount="indefinite" /-->
<animate id="ball-animation"
attributeName="cx"
attributeType="XML"
values = "0;285;570;285;0"
keyTimes="0;0.25;0.5;0.75;1"
calcMode="linear"
dur="5s"
repeatCount="indefinite"
/>
</circle>
<!-- mid-point -->
<line stroke-dasharray="5, 10" x1="287.5" y1="1" x2="287.5" y2="300" stroke="white" stroke-width="1" />
<!-- scores -->
<text x="261" y="27" font-family="monospace" font-size="22px" fill="white" id="score1">0</text>
<text x="298" y="27" font-family="monospace" font-size="22px" fill="white" id="score2">0</text>
<!-- paddles -->
<rect x="10" y="10" width="5" height="25" stroke="white" stroke-width="1" fill="white" id="paddle1" />
<rect x="561" y="10" width="5" height="25" stroke="white" stroke-width="1" fill="white" id="paddle2" />
</svg>
References
The solution is based on this SO question and the following standards documents:
animateMotionelement
SVG IDL
SVGAnimatedLength
Alternatives
anime.js appears to be a animation library that would suit needs outlined in the question (cf. this section), in partiucular for authors with a solid js background (I have no affiliation neither with the library nor the author).

Uncaught TypeError: Cannot read property 'childNodes' of null

Not sure why I get this error. Read about the root of the problem seems to exist when JavaScript executes before the element is loaded however in my case I use onload function on body tag. also the JavaScript is just before the body tag closes.Not sure what is causing this problem. **The error correspondes to second line of initLegend() function
this is the line the throws error
legendGroup = svgDoc.getElementById("legendGroup").childNodes;
Code
<body onload="init()">
<h1>Co2 Emissions</h1>
<div id="viz" style='margin-top: -5px;'></div>
<script type="text/javascript">
var changeArray = [-80,-60,-40,-20,0,20,40,60,80];
var colorArray = ["#053061", "#2166ac", "#4393c3", "#92c5de", "#F1F7FA", "#fddbc7", "#f4a582", "#d6604d", "#b2182b", "#67001f"];
var legend;
var legendGroup;
svgDoc = document;
var svg_0 = d3.xml("svg_drawing.svg", "image/svg+xml", function(xml) {
var importedNode = document.importNode(xml.documentElement, true);
d3.select("#viz").node().appendChild(importedNode)});
function initLegend()
{
legend = svgDoc.getElementById("legend");
legendGroup = svgDoc.getElementById("legendGroup").childNodes;
// set text for headline
var node = legend.childNodes.item(1);
node.firstChild.nodeValue = currentMonth + currentYear;
node = node.nextSibling.nextSibling;
node.firstChild.nodeValue = legendHeadline;
node = node.nextSibling.nextSibling;
node.firstChild.nodeValue = legendLabelDecrease;
node = node.nextSibling.nextSibling;
node.firstChild.nodeValue = legendLabelIncrease1;
node = node.nextSibling.nextSibling;
node.firstChild.nodeValue = legendLabelIncrease2;
// set legend items
if(debug) { alert("legendGroup.length: " + legendGroup.length); }
var rectInvariant = 0;
var textInvariant = 0;
for(var i=0; i<legendGroup.length; i++)
{
if(legendGroup.item(i))
{
if(legendGroup.item(i).nodeName == "rect")
{
legendGroup.item(i).setAttribute("fill",colorArray[rectInvariant++]);
} else
{
if(legendGroup.item(i).nodeName == "text")
{
legendGroup.item(i).firstChild.nodeValue = changeArray[textInvariant++] + "%";
}
}
}
}
}
function init(){
initLegend()
};
</script>
</body>
</html>
SVG og legend
<g id="legend" transform="matrix(1 0 0 1 685 28)">
<text id="legendHeadline" class="legendHeadline" x="87" y="-7">...</text>
<text id="legendSubheadline" class="legendSubheadline" x="95" y="-7">...</text>
<text id="decrease" class="legendLabel" x="-25" y="17">...</text>
<text id="increase1" class="legendLabel" x="379" y="10">...</text>
<text id="increase2" class="legendLabel" x="379" y="22">...</text>
<g id="legendGroup" transform="matrix(1 0 0 1 0 8)">
<rect x="0" y="0" width="35" height="10" stroke="#000000" stroke-width="1px" fill="#FFFFFF" />
<text x="35" y="27" class="legendStyle">...</text>
<rect x="35" y="0" width="35" height="10" stroke="#000000" stroke-width="1" fill="#FFFFFF" />
<text x="70" y="27" class="legendStyle">...</text>
<rect x="70" y="0" width="35" height="10" stroke="#000000" stroke-width="1px" fill="#FFFFFF" />
<text x="105" y="27" class="legendStyle">...</text>
<rect x="105" y="0" width="35" height="10" stroke="#000000" stroke-width="1px" fill="#FFFFFF" />
<text x="140" y="27" class="legendStyle">...</text>
<rect x="140" y="0" width="35" height="10" stroke="#000000" stroke-width="1px" fill="#FFFFFF" />
<text x="175" y="27" class="legendStyle">...</text>
<rect x="175" y="0" width="35" height="10" stroke="#000000" stroke-width="1px" fill="#FFFFFF" />
<text x="210" y="27" class="legendStyle">...</text>
<rect x="210" y="0" width="35" height="10" stroke="#000000" stroke-width="1px" fill="#FFFFFF" />
<text x="245" y="27" class="legendStyle">...</text>
<rect x="245" y="0" width="35" height="10" stroke="#000000" stroke-width="1px" fill="#FFFFFF" />
<text x="280" y="27" class="legendStyle">...</text>
<rect x="280" y="0" width="35" height="10" stroke="#000000" stroke-width="1px" fill="#FFFFFF" />
<text x="315" y="27" class="legendStyle">...</text>
<rect x="315" y="0" width="35" height="10" stroke="#000000" stroke-width="1px" fill="#FFFFFF" />
</g>
</g>
You need simply to move the initLegend() call into the callback handler, right after the point at which you append it. Loading the SVG is asynchronous, and the window "load" event will not wait for that to finish.
edit like this:
var svg_0 = d3.xml("svg_drawing.svg", "image/svg+xml", function(xml) {
var importedNode = document.importNode(xml.documentElement, true);
d3.select("#viz").node().appendChild(importedNode);
initLegend();
});

Categories