JavaScript SVG continously appending animated element to SVG element - javascript

I have SVG element and I'm dynamically with JavaScript creating Circle element with animateMotion element. Circle should dynamically follow along with SVG path. I'm attaching endEvent listener to AnimateMotion element so after completing animation, created Circle element should be removed from DOM and whole process should start again. It works fine for first animation, but not for another iterations. Code is below - where I made mistake?
const test = document.getElementById('test');
function createAnimation() {
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
const animation = document.createElementNS('http://www.w3.org/2000/svg', 'animateMotion');
circle.setAttributeNS(null, 'r', '8');
circle.setAttributeNS(null, 'fill', '#00F');
animation.setAttributeNS(null, 'dur', '2s');
animation.setAttributeNS(null, 'path', "M20,50 C20,-50 180,150 180,50 C180-50 20,150 20,50 z");
circle.appendChild(animation);
test.appendChild(circle);
animation.addEventListener('endEvent', () => {
circle.parentElement.removeChild(circle);
});
return new Promise((resolve) => {
animation.addEventListener('endEvent', () => {
resolve();
});
});
}
(async() => {
while (true) {
await createAnimation();
}
})();
<svg id="test" viewBox="0 0 200 100" xmlns="http://www.w3.org/2000/svg">
<path fill="none" stroke="lightgrey"
d="M20,50 C20,-50 180,150 180,50 C180-50 20,150 20,50 z" />
</svg>

SMIL animations are based on a global clock. By default, if you don't specify a begin, the animation will start at time 0. Where time 0 is when the document is created/displayed.
After the first run through of the animation (duration = 2s), the global timing clock will be at 2s.
At that point you add a new circle and <animateMotion> element. Its start time defaults to "0" also. Meaning that the browser considers that the run period for that animation has been and gone. That is because we are past the 2 sec mark by now.
Try the following:
Add begin="indefinite" to the animation. That tells the browser that you intend to start the animation yourself.
animation.setAttribute('begin', 'indefinite');
Trigger the animation after you have finished creating it.
animation.beginElement();
However...
Why keep creating more and more circles? Is there a reason for that? Why not just restart the animation when it is finished? Something like
createAnimation();
animation.addEventListener('endEvent', () => {
animation.beginElement();
});

Related

Svg animations replaying in the same spot

I am working on a tower defense game with HTML, CSS, and JS. I want to be able to create svg circles that will follow a path using an svg animation. In order to do this, I wrote this code:
<div id="trackPart">
<progress id="healthBar" max="200" value="200" onclick="this.value = randInt(0, this.max)" ></progress>
<svg viewbox="0 0 1000 3000" id="track" xmlns="http://www.w3.org/2000/svg">
<path id="path" fill="none" stroke="red" stroke-width="15" d="M 0, 50 h 900 v 100 h -800 v 100 h 800 v 300 h -900" style="cursor: pointer"/>
</svg>
</div>
var path = document.getElementById("path")
var svgurl = "http://www.w3.org/2000/svg"
var svg = document.getElementById("track")
listOfColors = ["green", "blue", "purple", "lime", "yellow"]
function attackerSVG(color) {
let element = document.createElementNS(svgurl, "circle")
element.setAttribute("cx", 0)
element.setAttribute("cy", 0)
element.setAttribute("r", 15)
element.setAttribute("fill", color)
let animation = document.createElementNS(svgurl, "animateMotion")
animation.setAttribute("dur", "30s")
animation.setAttribute("repeatCount", "indefinite")
animation.setAttribute("rotate", "auto")
animation.setAttribute("path", String(path.getAttribute("d")))
animation.setAttribute("onrepeat", "console.log(\"repeat\")")
animation.setAttribute("restart", "always")
animation.beginElement()
element.appendChild(animation)
svg.appendChild(element)
return element
}
attackerSVG("black")
The first time I run the attackerSvg function, everything works fine. A circle is created at the start of the path and follows it. However, once I create another one, it starts its animation sequence where the other svg circles are. If you want to see what I mean, you can go here
https://replit.com/#hello1964/Tower-defense-game#script.js
Whenever you see the circle change color, it's a new circle being created. When you look in the console it will print "repeat" every time a circle finishes a cycle. Since they are all in the same spot, it will print it multiple times. I would really appreciate the help, thank you.
SVG maintain their own timings. Adding an element to the SVG, will start it at the current clock. To do what you want, you actually need to delay the start
// outside globally somwhere
let count = 0;
// inside function attackerSVG
animation.setAttribute('begin', `${++count}s`)

How to catch SVG click events that hit the viewbox, rather than elements on it?

I have an SVG file with various paths, it is embedded into an HTML page using the object tag. Javascript is used to provide some interactivity to each path - when it is clicked, a tooltip rect is shown. This is what it looks like:
I want the tooltip to disappear when someone clicks outside of the path the tooltip is associated with, this is implemented by adding such an event listener to every path:
path.addEventListener("click", function(event){
if (!isTipShown()){
createTooltip()
}
else{
hideTooltip()
}
})
isTipShown, createTooltip and hideTooltip are functions that check the SVG DOM and modify it accordingly.
This works, but it fails if the click goes to the empty space between the paths themselves - because there is no object to catch it.
What approach can be chosen to implement such functionality?
My current thoughts:
Create a transparent rectangle that covers the entire viewport, and use that as a click target. How would one ensure the rectangle goes to the bottom of everything?
A click handler for the entire HTML document does the trick, but only if users click outside of the viewport itself.
Tooltip on shapes not my SVG!
How to best remove the tooltip when pressing the svg?
They way i would solve it is:
ID on the tooltip.
Modify the existing tooltip with the ID.
Remove tooltip with ID when pressed anywhere else
By reusing the tooltip, there can only be one tooltip on the page at one time.
Removing the tooltip (not deleting) makes it possible to reuse the same tooltip again when a new path is presed.
Here is an example:
document.addEventListener("DOMContentLoaded", function() {
var pathRed = document.getElementById("red");
var pathOrange = document.getElementById("orange");
var pathBlue = document.getElementById("blue");
var paths = [pathRed, pathOrange, pathBlue];
var toolTip = document.createElement("div");
toolTip.id = "toolTip";
var svg = document.getElementById("box");
var shown = false;
paths.forEach(function(element) {
element.addEventListener("click", function(event) {
if (shown == false) {
toolTip.innerText = element.id;
toolTip.style.top = (event.pageY) + "px";
toolTip.style.left = (event.pageX) + "px";
document.body.appendChild(toolTip);
shown = true;
//Only click the path
event.stopPropagation();
} else {
removeToolTip();
}
});
});
svg.addEventListener("click", function(event) {
removeToolTip();
event.preventDefault();
});
function removeToolTip() {
shown = false;
if (document.body.contains(toolTip)) {
document.body.removeChild(toolTip);
}
}
});
#toolTip {
position: absolute;
background-color: #00000099;
padding: 2px;
border-radius: 2px;
color: white;
font-size: 20px;
}
<h1>Click the boxes!</h1>
<svg id="box" viewBox="0 0 15 15" width="250px">
<path id="red" fill="red" d="m0,0 5,0 0,5 -5,0Z"/>
<path id="orange" fill="orange" d="m5,5 5,0 0,5 -5,0Z"/>
<path id="blue" fill="blue" d="m10,10 5,0 0,5 -5,0Z"/>
</svg>
The solution is to make sure the rectangle goes below the paths, as if it were a bottom layer.
SVG does not have a concept of layers, but it can be achieved by making sure that the rect is before all the elements in the SVG DOM, and all subsequent elements will be placed on top of it, visually:
<rect x="0" y="0" width="30" height="30" fill="purple"/>
<rect x="20" y="5" width="30" height="30" fill="blue"/>
<rect x="40" y="10" width="30" height="30" fill="green"/>
<rect x="60" y="15" width="30" height="30" fill="yellow"/>
<rect x="80" y="20" width="30" height="30" fill="red"/>
Here is how this was accomplished in practice (the svgDoc variable is the root SVG element):
function createBackgroundRectangle(svgDoc){
var rect = svgDoc.createElementNS("http://www.w3.org/2000/svg", 'rect')
rect.setAttributeNS(null, 'height', 500)
rect.setAttributeNS(null, 'width', 900)
rect.setAttributeNS(null, 'id', 'pseudo-background')
rect.setAttributeNS(null, 'x', 0)
rect.setAttributeNS(null, 'y', 0)
// the opacity is set to 0, so it doesn't get in the way visually. For debugging
// purposes, you can change it to another value and see the actual rectangle
rect.setAttributeNS(null, 'style', 'opacity:0;fill:#ffd42a;fill-opacity:1;')
svgDoc.rootElement.insertBefore(rect, svgDoc.rootElement.children[0])
}
svgDoc.rootElement.insertBefore(rect, svgDoc.rootElement.children[0]) makes it the first, as it is inserted before the current child at index 0.

Snap.svg: how to make scaled group stays in place?

I have a simple SVG file like this :
<svg id="mySVG" /*other attributes"*/>
<group id="mixUps">
<ellipse /*ellipse atributtes blabla*/ />
<path /*this is a star-shaped path*/ />
<rect /*rect attributes*/ />
/*
Basically this is just a simple SVG group containing an ellipse, a path and a rectangle
*/
</g>
</svg>
and then, in the script file, I want this group to be scaled twice it's original size at mouseover event, and return to it's original size at mouseout :
<script>
var mySvg = Snap("#mySvg");
var mixUps = mySvg.select("#mixUps");
function mixCursor(evt){
if(evt.type==="mouseover"){
mixUps.animate({transform:"s2"}, 250);
}else if(evt.type==="mouseout"){
mixUps.animate({transform:"s1"}, 250);
}
}
mixUps.mouseover(mixCursor);
mixUps.mouseout(mixCursor);
</script>
However, at the first mouseover event, the group somehow translated (moved) to upper-left corner, and stays there, why is this? How to make this group stays in place when scaled?
I put the file here.
You need to include the initial transform that's in place, otherwise it will get overwritten, so you want original transform THEN new transform, so instead of
mixUps.animate({transform: 's2'}, 250);
use
mixUps.animate({transform: this.transform() + 's2'}, 250);
jsfiddle
Or better is to store the original transform, so we can revert back to it later...
jsfiddle
mixUps.data('originalTransform', mixUps.transform() )
...
mixUps.animate({transform: this.data('originalTransform') + 's2'}, 250);
...
mixUps.animate({transform: this.data('originalTransform') }, 250);

How to trigger mouseover event on stacked SVG elements

While the CSS tag pointer-events:none will make an SVG element not be the target of mouse events, only one stacked element can be the target of the event. Is there a simple way to make mouse events target ALL elements that are under the cursor, so that a stacked set of elements will all have their events triggered if the event occurs within their bounding box?
There is a method on SVGSVGElement (<svg>) called getIntersectionList() which will return all elements that intersect with a given rectangle.
As an example, here is a code snippet which will give every SVG shape under the click a random colour.
var mysvg = document.getElementById("mysvg");
mysvg.addEventListener('click', function(evt) {
var hitRect = mysvg.createSVGRect();
hitRect.x = evt.clientX;
hitRect.y = evt.clientY;
// (leave width & height as 0)
var elems = mysvg.getIntersectionList(hitRect, null);
for (i=0; i<elems.length; i++) {
// Give each element under the mouse a random fill colour
elems.item(i).setAttribute('fill', "rgb("+rnd()+","+rnd()+","+rnd()+")");
}
});
function rnd() {
return Math.floor(Math.random() * 255.99);
}
<svg id="mysvg">
<rect width="150" height="100" fill="#393"/>
<rect x="120" y="20" width="140" height="130" fill="orange"/>
<rect x="220" y="0" width="80" height="130" fill="red"/>
</svg>
Unfortunately, this currently only works in Chrome (maybe Safari also?). Supposedly FF implemented partial support for this, but it currently seems to have been removed.

SVG element inserted into DOM is ignored (its type is changed)

I am using the VivaGraph.js library to render a graph in SVG. I am trying to display an image cropped to a circle, for which I am using a clipPath element - as recommended in this post.
However, when I create a new SVG element of type that has a capital letter in it, e.g. clipPath in my case, the element that is inserted into the DOM is lowercase, i.e. clippath, even though the string I pass in to the constructor is camelCase. Since SVG is case sensitive, this element is ignored. Everything else seems to be okay.
I also tried to change the order in which I append the child elements, in hopes of changing the 'z-index', but it didn't have an impact on this.
I am using the following code inside of the function that creates the visual representation of the node in the graph (the 'addNode' callback) to create the node:
var clipPhotoId = 'clipPhoto';
var clipPath = Viva.Graph.svg('clipPath').attr('id', clipPhotoId);
var ui = Viva.Graph.svg('g');
var photo = Viva.Graph.svg('image').attr('width', 20).attr('height', 20).link(url).attr('clip-path', 'url(#' + clipPhotoId + ')');
var photoShape = Viva.Graph.svg('circle').attr('r', 10).attr('cx', 10).attr('cy', 10);
clipPath.append(photoShape);
ui.append(clipPath);
ui.append(photo);
return ui;
Thank you!
There is a bit of tweaking needed on top of the post you provided.
General idea to solve your issue is this one:
We create a VivaGraph svg graphics (which will create an svg element in the dom)
Into this svg graphic we create only once a clip path with relative coordinates
When we create a node we refer to the clip path
Code is:
var graph = Viva.Graph.graph();
graph.addNode('a', { img : 'a.jpg' });
graph.addNode('b', { img : 'b.jpg' });
graph.addLink('a', 'b');
var graphics = Viva.Graph.View.svgGraphics();
// Create the clipPath node
var clipPath = Viva.Graph.svg('clipPath').attr('id', 'clipCircle').attr('clipPathUnits', 'objectBoundingBox');
var circle = Viva.Graph.svg('circle').attr('r', .5).attr('cx', .5).attr('cy', .5);
clipPath.appendChild(circle);
// Add the clipPath to the svg root
graphics.getSvgRoot().appendChild(clipPath);
graphics.node(function(node) {
return Viva.Graph.svg('image')
.attr('width', 30)
.attr('height', 30)
// I refer to the same clip path for each node
.attr('clip-path', 'url(#clipCircle)')
.link(node.data.img);
})
.placeNode(function(nodeUI, pos){
nodeUI.attr('x', pos.x - 15).attr('y', pos.y - 15);
});
var renderer = Viva.Graph.View.renderer(graph, { graphics : graphics });
renderer.run();
The result in the dom will be like this:
<svg>
<g buffered-rendering="dynamic" transform="matrix(1, 0, 0,1,720,230.5)">
<line stroke="#999" x1="-77.49251279562495" y1="-44.795726056131116" x2="6.447213894549255" y2="-56.29464520347651"></line>
<image width="30" height="30" clip-path="url(#clipCircle)" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="a.jpg" x="-92.49251279562495" y="-59.795726056131116"></image>
<image width="30" height="30" clip-path="url(#clipCircle)" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="b.jpg" x="-8.552786105450746" y="-71.2946452034765"></image>
</g>
<clipPath id="clipCircle" clipPathUnits="objectBoundingBox">
<circle r="0.5" cx="0.5" cy="0.5"></circle>
</clipPath>
</svg>
Notice the clipPathUnits="objectBoundingBox", since it's the main trick for this solution.

Categories

Resources