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`)
Related
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();
});
I am trying to figure out a case where I have a vertical green line that represents a stack of layers and a red "plank" that represents currently focused layer out of the stack.
I need to make sure when top most layer is selected - the plank is at the top of the green line. When first (deepest) layer is selected the plank is at the bottom of the green line.
Using the function below I can achieve the #1 condition, but whenever I select deepest layer - I can't get red plank placed at the bottom.
I know this should be school level math exercise but I cant figure it out;/
UPDATE: so in the snippet if I will do current Layer 1 and total N/whatever. I cant "land" the plank onto the bottom of the line
UPDATE2: OK I added some more logic to the function and now the result is what I need. PRoblem is I am not sure I achieved this through proper logic. At least I don't really understand what I did and why it now works.
// trying to figure out better function to indicate which is current layer:
// height of the green line is 280.
var currentItemLayer = 3;
var totalLayers = 4;
var plank = document.getElementById("plank");
setPlankPositionAccordingToLayer = function(currentItemLayer, totalLayers) {
var blockCount = Math.max(1, totalLayers - 1);
var blockSize = 280*(1/blockCount);
var layer;
if (blockCount === 1) {
layer = 100;
} else {
layer = 380-(currentItemLayer*blockSize);
}
plank.setAttribute("y", layer);
console.log(layer);
};
setPlankPositionAccordingToLayer(currentItemLayer, totalLayers)
<body>
<svg id="mainSVG"
viewBox="0 0 480 480"
preserveAspectRatio="xMidYMin"
width="100%"
height="100%"
background-color="white"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<line x1="240" y1="100" x2="240" y2="380" style="stroke: green"/>
<rect id="plank" x="230" y="240" width="20" height="2"/>
</svg>
</body>
I want to create a growing pie animation using Javascript and SVG embedded in HTML. Input should be percentage and output should be an image. It should animate like this:
This should work like GUI mouse hold action feedback (user needs long press something). This is also why I can't use GIF animation as the timeout may vary.
I tried to make this in Inkscape and then reverse-engineer the XML but I don't understand it at all. There's a <path> node which has property d full of gibberish numbers:
d="m 250.78761,446.46564 a 28.183382,28.183382 0 0 1 -24.596,27.95413 28.183382,28.183382 0 0 1 -30.85751,-20.83773"
I assume these are some points of path. But can't I just make circle and mention percentage of how it's full? How are these points even generated?
This is what I played with:
body, html {
padding: 0px;
margin: 0px;
border: 1px solid grey;
}
svg {
/** to fit svg in the viewbox**/
max-height: 400px;
border: 1px solid black;
}
<svg class="test" viewBox="-20 -20 1000 1000">
<path
id="circle4146"
style="stroke:#61a4ff;stroke-width:15.00000095;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;"
sodipodi:type="arc"
sodipodi:cx="140.71873"
sodipodi:cy="446.46564"
sodipodi:rx="28.183382"
sodipodi:ry="28.183382"
sodipodi:start="0"
sodipodi:end="1.1720792"
sodipodi:open="true"
d="m 168.90212,446.46564 a 28.183382,28.183382 0 0 1 -17.24157,25.97267" />
</svg>
The sodipodi stuff is probably used by inkscape, changing it has no effect. I know that the d attribute describes complex path. What I really need is for someone to highlight me which points should be moved (using sin and cos I assume) to achieve desired effect.
Also I was unable to adjust the viewport to the circle. Apparently some of the coordinates are not X and Y.
Something like this? Here I am just calculating what the length of the circumference that the percentage represents. Then I give the circle a stroke dash pattern with that length and a big gap.
setCircleTo(70);
function setCircleTo(percent)
{
// Get the circle element via the DOM
var circle = document.getElementById('mycircle');
// Calculate the circle circumference via the circles 'r' attribute
var circumference = Math.PI * 2 * circle.r.baseVal.value;
// Calculate what <percent> of the circumference is
var adjustedLen = percent * circumference / 100;
// Set the circle's dashpattern one with a dash that length
circle.setAttribute('stroke-dasharray', adjustedLen+' '+circumference);
}
<svg class="test" viewBox="-20 -20 1000 1000">
<circle id="mycircle" cx="100" cy="75" r="50" stroke-width="30" fill="none" stroke="#61a4ff"/>
</svg>
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.
I'm developing a map, in Javascript using SVG to draw the lines.
I would like to add a feature where you can search for a road, and if the road is found, a circle appears on the map.
I know i can draw a circle in SVG, but my problem is that, the size of the circle should not change depending on the zoom-level. In other words the circle must have the same size at all times.
The roads on my map have this feature, all i had to do was add
vector-effect="non-scaling-stroke"
to the line attributes..
A line looks like this.
<line vector-effect="non-scaling-stroke" stroke-width="3" id = 'line1' x1 = '0' y1 = '0' x2 = '0' y2 = '0' style = 'stroke:rgb(255,215,0);'/>
The circle looks like this.
<circle id = "pointCircle" cx="0" cy="0" r="10" stroke="red" stroke-width="1" fill = "red"/>
Is it possible to define the circle as "non-scaling" somehow?
It took me a while, but I finally got the math clean. This solution requires three things:
Include this script in your page (along with the SVGPan.js script), e.g.
<script xlink:href="SVGPanUnscale.js"></script>
Identify the items you want not to scale (e.g. place them in a group with a special class or ID, or put a particular class on each element) and then tell the script how to find those items, e.g.
unscaleEach("g.non-scaling > *, circle.non-scaling");
Use transform="translate(…,…)" to place each element on the diagram, not cx="…" cy="…".
With just those steps, zooming and panning using SVGPan will not affect the scale (or rotation, or skew) of marked elements.
Demo: http://phrogz.net/svg/scale-independent-elements.svg
Library
// Copyright 2012 © Gavin Kistner, !#phrogz.net
// License: http://phrogz.net/JS/_ReuseLicense.txt
// Undo the scaling to selected elements inside an SVGPan viewport
function unscaleEach(selector){
if (!selector) selector = "g.non-scaling > *";
window.addEventListener('mousewheel', unzoom, false);
window.addEventListener('DOMMouseScroll', unzoom, false);
function unzoom(evt){
// getRoot is a global function exposed by SVGPan
var r = getRoot(evt.target.ownerDocument);
[].forEach.call(r.querySelectorAll(selector), unscale);
}
}
// Counteract all transforms applied above an element.
// Apply a translation to the element to have it remain at a local position
function unscale(el){
var svg = el.ownerSVGElement;
var xf = el.scaleIndependentXForm;
if (!xf){
// Keep a single transform matrix in the stack for fighting transformations
// Be sure to apply this transform after existing transforms (translate)
xf = el.scaleIndependentXForm = svg.createSVGTransform();
el.transform.baseVal.appendItem(xf);
}
var m = svg.getTransformToElement(el.parentNode);
m.e = m.f = 0; // Ignore (preserve) any translations done up to this point
xf.setMatrix(m);
}
Demo Code
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Scale-Independent Elements</title>
<style>
polyline { fill:none; stroke:#000; vector-effect:non-scaling-stroke; }
circle, polygon { fill:#ff9; stroke:#f00; opacity:0.5 }
</style>
<g id="viewport" transform="translate(500,300)">
<polyline points="-100,-50 50,75 100,50" />
<g class="non-scaling">
<circle transform="translate(-100,-50)" r="10" />
<polygon transform="translate(100,50)" points="0,-10 10,0 0,10 -10,0" />
</g>
<circle class="non-scaling" transform="translate(50,75)" r="10" />
</g>
<script xlink:href="SVGPan.js"></script>
<script xlink:href="SVGPanUnscale.js"></script>
<script>
unscaleEach("g.non-scaling > *, circle.non-scaling");
</script>
</svg>
If you are looking for a fully static way of doing this, you might be able to combine non-scaling-stroke with markers to get this, since the markers can be relative to the stroke-width.
In other words, you could wrap the circles in a <marker> element and then use those markers where you need them.
<svg width="500" height="500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2000 2000">
<marker id="Triangle"
viewBox="0 0 10 10" refX="0" refY="5"
markerUnits="strokeWidth"
markerWidth="4" markerHeight="3"
orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" />
</marker>
<path d="M 100 100 l 200 0" vector-effect="non-scaling-stroke"
fill="none" stroke="black" stroke-width="10"
marker-end="url(#Triangle)" />
<path d="M 100 200 l 200 0"
fill="none" stroke="black" stroke-width="10"
marker-end="url(#Triangle)" />
</svg>
The same can also be viewed and tweaked here. The svg spec isn't fully explicit about what should happen in this case (since markers are not in SVG Tiny 1.2, and vector-effect isn't in SVG 1.1). My current line of thinking was that it should probably affect the size of the marker, but it seems no viewers do that at the moment (try in a viewer that supports vector-effect, e.g Opera or Chrome).
Looks like some work was done in webkit (maybe related to this bug: 320635) and the new transform doesn't stick around when simply appended like that
transform.baseVal.appendItem
This seems to work better. Even works in IE 10.
EDIT: Fixed the code for more general case of multiple translate transformations in the front and possible other transformations after. First matrix transformation after all translates must be reserved for unscale though.
translate(1718.07 839.711) translate(0 0) matrix(0.287175 0 0 0.287175 0 0) rotate(45 100 100)
function unscale()
{
var xf = this.ownerSVGElement.createSVGTransform();
var m = this.ownerSVGElement.getTransformToElement(this.parentNode);
m.e = m.f = 0; // Ignore (preserve) any translations done up to this point
xf.setMatrix(m);
// Keep a single transform matrix in the stack for fighting transformations
// Be sure to apply this transform after existing transforms (translate)
var SVG_TRANSFORM_MATRIX = 1;
var SVG_TRANSFORM_TRANSLATE = 2;
var baseVal = this.transform.baseVal;
if(baseVal.numberOfItems == 0)
baseVal.appendItem(xf);
else
{
for(var i = 0; i < baseVal.numberOfItems; ++i)
{
if(baseVal.getItem(i).type == SVG_TRANSFORM_TRANSLATE && i == baseVal.numberOfItems - 1)
{
baseVal.appendItem(xf);
}
if(baseVal.getItem(i).type != SVG_TRANSFORM_TRANSLATE)
{
if(baseVal.getItem(i).type == SVG_TRANSFORM_MATRIX)
baseVal.replaceItem(xf, i);
else
baseVal.insertItemBefore(xf, i);
break;
}
}
}
}
EDIT2:
Chrome killed getTransformToElement for some reason, so the matrix needs to be retrieved manually:
var m = this.parentNode.getScreenCTM().inverse().multiply(this.ownerSVGElement.getScreenCTM());
It's discussed here and here
It looks like current browsers don't do the expected thing, so one needs to apply the inverse transform of the zoom (scale) on the contents of the <marker>, eg. transorm: scaleX(5) on the user of the <marker> etc. will need to be accompanied by a transform: translate(...) scaleX(0.2) inside the <pattern>, also factoring in possible x/y/width/height/transform-origin values inside the pattern if needed