How to change CSS class to an <use> children inside a SVG? - javascript

I am making and animation the objetive is change the xlink:href inside a SVG. (this is for change a shape), and change class respect to their position inside.
This is my SVG
<svg viewBox="-20 -20 600 200" id="main">
<defs id="test">
<rect width="80" height="80" id="circle" fill="red" class="first" />
<rect width="80" height="80" id="square" fill="pink" class="second" />
<rect width="80" height="80" id="cross" fill="blue" class="third" />
</defs>
<g id="load-area">
<use x="0" xlink:href="#circle" />
<use x="100" xlink:href="#square" />
<use x="200" xlink:href="#cross" />
</g>
</svg>
The class in every rectelement, has a different animation-delay according to position (first execute at 0s, second at 2s, third at 4s and so on).
With JS I change every <use> at #load-area
main.children['load-area'].children[0].setAttribute("xlink:href", getFigure(random()));
And it works, the shape changes but, suppose when it gets three times the id #cross then all elements have third CSS class.
I need change CSS class inside every children of <use>, How can I do that?
Below an element tree :
I get all <use> with: main.children['load-area'].children but it does not have child element, as I show u below:

You can solve this using CSS variables that you combine with nth-child selector and you no more need the classes.
Here is a basic example
rect {
animation:change 3s var(--d,0s) infinite;
}
#keyframes change {
0% {
opacity:1;
}
33%,100% {
opacity:0;
}
}
#load-area > use:nth-child(1) {--d:0s}
#load-area > use:nth-child(2) {--d:1s}
#load-area > use:nth-child(3) {--d:2s}
/*#load-area > use:nth-child(N) {--d:Xs}*/
<svg viewBox="-20 -20 600 200" id="main">
<defs id="test">
<rect width="80" height="80" id="circle" fill="red" />
<rect width="80" height="80" id="square" fill="pink" />
<rect width="80" height="80" id="cross" fill="blue" />
</defs>
<g id="load-area">
<use x="0" xlink:href="#circle" />
<use x="100" xlink:href="#square" />
<use x="200" xlink:href="#cross" />
</g>
</svg>
<svg viewBox="-20 -20 600 200" id="main">
<g id="load-area">
<use x="0" xlink:href="#square" />
<use x="100" xlink:href="#circle" />
<use x="200" xlink:href="#cross" />
</g>
</svg>
If the number is unknown or very big you can easily use a JS loop:
var e = document.querySelectorAll('#load-area use');
for(var i=0;i<e.length;i++) {
e[i].style.setProperty('--d',i+"s");
}
rect {
animation:change 3s var(--d,0s) infinite;
}
#keyframes change {
0% {
opacity:1;
}
33%,100% {
opacity:0;
}
}
<svg viewBox="-20 -20 600 200" id="main">
<defs id="test">
<rect width="80" height="80" id="circle" fill="red" />
<rect width="80" height="80" id="square" fill="pink" />
<rect width="80" height="80" id="cross" fill="blue" />
</defs>
<g id="load-area">
<use x="0" xlink:href="#circle" />
<use x="100" xlink:href="#square" />
<use x="200" xlink:href="#cross" />
</g>
</svg>

document.getElementsByTagName("use")[0].setAttribute("xlink:href", "#circle");
element.setAttribute(attributename, attributevalue)
Here is how you can change element attributes

Related

How to animate svg height with CSS upward?

I am working with a svg element and I want to animate the height increasing upwards of these elements. But they are growing downward from the top probably due to SVG coordinate system. How can I make it go upwards from the bottom?
window.onload = function () {
var x1 = document.querySelector('svg').viewBox.baseVal.height;
var a = document.getElementById('wrapper1');
var bgArray = [];
for (var i = 1; i < a.children.length; i++) {
bgArray.push((a.children[i].getBBox().height / x1) * 100);
};
bgArray.forEach((x, i) => a.children[i + 1].style.setProperty("--h1", x + '%'));
bgArray.forEach((x, i) => a.children[i + 1].style.setProperty("--del", (i + 1) + 's')); //for staggered
}
.r1,
.r2 {
visibility: hidden;
animation: moveHeight 2s ease-in var(--del) 1 forwards;
}
#keyframes moveHeight {
0% {
visibility: visible;
height: 0%;
}
100% {
visibility: visible;
height: var(--h1);
}
}
<!DOCTYPE html>
<html>
<body>
<link rel="stylesheet" href="style.css">
</link>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 720">
<g class="wrapper1" id="wrapper1" >
<rect x="10" y="20" width="120" height="120" stroke="black" fill="none"></rect>
<rect class="r1" id="r1" x="10" y="80" width="20" height="60" stroke="none" fill="orange"></rect>
<rect class="r2" id="r2" x="31" y="100" width="20" height="40" stroke="none" fill="green"></rect>
<!---->
</g>
</svg>
<script src="index.js"></script>
</body>
</html>
You could draw the bars but cover them and then gradually uncover them.
This covers them by adding white bars and shrinking those.
window.onload = function() {
var x1 = document.querySelector('svg').viewBox.baseVal.height;
var a = document.getElementById('wrapper1');
var bgArray = [];
for (var i = 1; i < a.children.length; i++) {
bgArray.push((a.children[i].getBBox().height / x1) * 100);
};
bgArray.forEach((x, i) => a.children[i + 1].style.setProperty("--h1", x + '%'));
bgArray.forEach((x, i) => a.children[i + 1].style.setProperty("--del", (i + 1) + 's')); //for staggered
}
.shrink {
animation: moveHeight 2s ease-in var(--del) 1 forwards;
}
#keyframes moveHeight {
0% {
height: var(--h1);
}
100% {
height: 0;
}
}
<!DOCTYPE html>
<html>
<body>
<link rel="stylesheet" href="style.css">
</link>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 720">
<g class="wrapper1" id="wrapper1" >
<rect x="10" y="20" width="120" height="120" stroke="black" fill="none"></rect>
<rect class="r1" id="r1" x="10" y="80" width="20" height="60" stroke="none" fill="orange"></rect>
<rect class="r1 shrink" x="10" y="80" width="20" height="60" stroke="none" fill="white"></rect>
<rect class="r2" id="r2" x="31" y="100" width="20" height="40" stroke="none" fill="green"></rect>
<rect class="r2 shrink" x="30" y="99" width="22" height="41" stroke="none" fill="white"></rect>
<!---->
</g>
</svg>
<script src="index.js"></script>
</body>
</html>
Note, at some zoom levels a screen pixel of the underneath bars can be left showing (giving a faint vertical line at the start). To get round that the second white bar has been moved one left and made one wider (and similarly for height). Somewhat hacky so I hope someone can come up with a more straightforward solution.
I have looked into this brilliant answer but I had trouble visualizing and adapting it to my scenario. So after I figured out, I am trying to visually explain and show how to adapt to different scenarios.
The basic markup is following
<!DOCTYPE html>
<html>
<body>
<link rel="stylesheet" href="style.css">
</link>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 720">
<rect class="wrapper1" id="wrapper1" x="10" y="345" width="120" height="180" stroke="teal" fill="none"></rect>
<line class="upper clone" x1="10" y1="80" x2="220" y2="80" style="stroke:rgb(110, 31, 194);stroke-width:1" />
<g class="elements">
<line class="upper" x1="10" y1="80" x2="220" y2="80" style="stroke:rgb(87, 202, 91);stroke-width:1"/>
<rect class="r0" id="r0" x="10" y="80" width="83" height="180" stroke="brown" fill="none"></rect>
<rect class="r1" id="r1" x="10" y="80" width="20" height="60" stroke="none" fill="orange"></rect>
<rect class="r2" id="r2" x="31" y="80" width="20" height="10" stroke="none" fill="green"></rect>
<rect class="r3" id="r3" x="52" y="80" width="20" height="100" stroke="none" fill="blue"></rect>
<rect class="r4" id="r4" x="73" y="80" width="20" height="140" stroke="none" fill="magenta"></rect>
<line class="lower" x1="10" y1="260" x2="220" y2="260" style="stroke:rgb(87, 202, 91);stroke-width:1"/>
</g>
<line class="lower clone" x1="10" y1="260" x2="220" y2="260" style="stroke:rgb(218, 149, 22);stroke-width:1" />
<g class="wrapper2" id="wrapper2">
<rect x="140" y="0" width="50" height="700" stroke="red" fill="none"></rect>
</g>
</svg>
<script src="index2.js"></script>
</body>
</html>
For ease of understanding, I have created the two lines called upper and lower which are the start and end of all the rects. I have also cloned those lines where the transform will not apply.
The thumb rule of converting svg coordinate system to cartesian coordinate is to
use this transform="translate(0, maxY) scale(1, -1)".
The maxY is actually max total of (y+height). In this case it is 260. now this value can be manipulated to correctly place the rects prior to animation
<g class="elements" transform="translate(0,260) scale(1, -1)"> positions
<g class="elements" transform="translate(0,340) scale(1, -1)"> 260+rectY(80)=340 positions
<g class="elements" transform="translate(0,425) scale(1, -1)"> 260+rectY(80)+(wrapperY 345 -260) positions
and lastly positions at the bottom of the wrapper
<g class="elements" transform="translate(0,605) scale(1, -1)"> 260+rectY(80)+(wrapperY 345 -260)+wrapperHeight 180 = 605
Putting everything together with javascript and css
//converting svg coordinates to cartesian coordinates
// transform="translate(0, maxY) scale(1, -1)".
// or transform="scale(1, -1) translate(0, -maxY)".
window.onload = function app() {
var bgArray1 = []; //to collect only height
var bgArray2 = []; //to collect height + y
var wrapperElement = document.getElementById('wrapper1');
var wrapperY = wrapperElement.getBBox().y //what is the y coordinate of wrapper rect
var wrapperHeight = wrapperElement.getBBox().height;
var getRect = document.querySelectorAll('[class^="r"]');
for (var i = 0; i < getRect.length; i++) {
var _bBox = getRect[i].getBBox(); // getting the bBox for each rect
var _height = _bBox.height + _bBox.y //getting each rect's height
var _y = _bBox.y // getting each rect's y coordinate
var _heightAndY = _height + _y //total of Y+height of each rect
bgArray1.push(_height);
bgArray2.push(_heightAndY);
}
var _maxHeight = Math.max(...bgArray1); // use this if you want to position the rect at (y=0)
var _maxTotal = Math.max(...bgArray2); // use this if you want to position the rect at (y=0)
var _wrapperTop = _maxTotal + (wrapperY - _maxHeight); //use this if you want to postion the rect at the the
//top of the wrapper
var _wrapperBottom = _wrapperTop + wrapperHeight //use this if you wantto position the rects at the
//bottom of the wrapper
bgArray1.forEach((x, i) => getRect[i].setAttribute("transform", `translate(0,${_wrapperBottom}) scale(1,-1)`));
bgArray1.forEach((x, i) => getRect[i].style.setProperty("--del", (i + 1) + 's'))
}
[class^="r"] {
visibility: hidden;
animation: moveHeight 2s ease-out var(--del) 1 forwards;
}
#keyframes moveHeight {
0% {
visibility: visible;
height: 0
}
100% {
visibility: visible;
height: 100;
}
}
<!DOCTYPE html>
<html>
<body>
<link rel="stylesheet" href="style.css">
</link>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 720">
<rect class="wrapper1" id="wrapper1" x="10" y="345" width="120" height="180" stroke="teal" fill="none"></rect>
<line class="upper clone" x1="10" y1="80" x2="220" y2="80" style="stroke:rgb(110, 31, 194);stroke-width:1" />
<g>
<line class="upper" x1="10" y1="80" x2="220" y2="80" style="stroke:rgb(87, 202, 91);stroke-width:1"/>
<rect class="r0" id="r0" x="10" y="80" width="83" height="180" stroke="brown" fill="none"></rect>
<rect class="r1" id="r1" x="10" y="80" width="20" height="60" stroke="none" fill="orange"></rect>
<rect class="r2" id="r2" x="31" y="80" width="20" height="10" stroke="none" fill="green"></rect>
<rect class="r3" id="r3" x="52" y="80" width="20" height="100" stroke="none" fill="blue"></rect>
<rect class="r4" id="r4" x="73" y="80" width="20" height="140" stroke="none" fill="magenta"></rect>
<line class="lower" x1="10" y1="260" x2="220" y2="260" style="stroke:rgb(87, 202, 91);stroke-width:1"/>
</g>
<line class="lower clone" x1="10" y1="260" x2="220" y2="260" style="stroke:rgb(218, 149, 22);stroke-width:1" />
<g class="wrapper2" id="wrapper2">
<rect x="140" y="0" width="50" height="700" stroke="red" fill="none"></rect>
</g>
</svg>
<!--<script src="index.js"></script>-->
<script src="index2.js"></script>
</body>
</html>
Pure svg solution
The idea is to first hide the rectangles behind a masking strip.
Then raise them to the top by animating the y attribute of the rectangle
<animate id="anR1" xlink:href="#r1" attributeName="y" begin="Layer_1.click"
dur="1s" from="140" to="80" repeatCount="1" fill="freeze" />
<!DOCTYPE html>
<html>
<body>
<link rel="stylesheet" href="style.css">
</link>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 720">
<g class="wrapper1" id="wrapper1" >
<rect x="10" y="20" width="120" height="120" stroke="black" fill="none"></rect>
<rect class="r1" id="r1" x="10" y="140" width="20" height="60" stroke="none" fill="orange"></rect>
<rect class="r2" id="r2" x="31" y="140" width="20" height="40" stroke="none" fill="green"></rect>
<rect class="r3" id="r3" x="52" y="140" width="20" height="80" stroke="none" fill="dodgerblue"></rect>
<!-- masking strip -->
<rect x="10" y="140" width="120" height="120" fill="white"></rect>
</g>
<animate id="anR1" xlink:href="#r1" attributeName="y" begin="Layer_1.click" dur="0.5s" from="140" to="80" repeatCount="1" fill="freeze" />
<animate id="anR2" xlink:href="#r2" attributeName="y" begin="anR1.end+0.25s" dur="0.5s" from="140" to="100" repeatCount="1" fill="freeze" />
<animate id="anR3" xlink:href="#r3" attributeName="y" begin="anR2.end+0.25s" dur="0.5s" from="140" to="60" repeatCount="1" fill="freeze" />
</svg>
<script src="index.js"></script>
</body>
</html>

Variable text in SVG defined text tag

I would like to re-use def-ined shapes in SVG but with variable text.
Is the following somehow possibel ?
<svg width="1000pt" height="1000pt" viewBox="0.00 0.00 1000.00 1000.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<g id="shape">
<rect x="10" y="0" rx="5" ry="5" width="80" height="40" style="fill:lightblue;stroke-width:0,opacity:0.5" />
<text text-anchor="middle" x="40" y="20" font-family="Helvetica,sans-Serif" font-size="8.00">variable_text</text>
</g>
</defs>
<g transform="translate(0 0)">
<use xlink:href="#shape" text="test" />
</g>
<g transform="translate(100 0)">
<use xlink:href="#shape" text="test2" />
</g>
</svg>
EDIT: since the solution would probably involve some javascript I have added the tag.
Clone the template and adjust as necessary.
let shape = document.getElementById("shape");
Array.from(document.getElementsByTagName("use")).forEach((use) => {
let text = use.getAttribute("text");
let clone = shape.cloneNode(true);
// might want to do something more robust here
clone.children[1].textContent = text;
use.parentNode.appendChild(clone);
use.parentNode.removeChild(use);
})
<svg width="1000pt" height="1000pt" viewBox="0.00 0.00 1000.00 1000.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<g id="shape">
<rect x="10" y="0" rx="5" ry="5" width="80" height="40" style="fill:lightblue;stroke-width:0,opacity:0.5" />
<text text-anchor="middle" x="40" y="20" font-family="Helvetica,sans-Serif" font-size="8.00">variable_text</text>
</g>
</defs>
<g transform="translate(0 0)">
<use xlink:href="#shape" text="test" />
</g>
<g transform="translate(100 0)">
<use xlink:href="#shape" text="test2" />
</g>
</svg>

Center elements inside SVG relative to each other-

Couldn't find it anywhere else to I might ask here :
I have svg rectangle which inside has other rectangles.
What I want to achieve is to center each of these rectangles in the centre of it's parent and in relation to each other.
If I drag rectangle 1 down I want the other one to move up to keep both of them centered - and same thing happening if I drag the other element down (should push upper one up).
Problem here is that I might have different width/heights and there would be 2 or more elements. Is there any mathematical equationfor that? Or a name that I can look for?
I would put the 3 rects inside a group and use the group as in the following example
svg{
border:1px solid;
width: 30vh;
}
<svg viewBox="0 0 100 280">
<defs>
<g id="rects">
<rect width="80" height="80" />
<rect width="60" height="25" x="10" y="10" />
<rect width="60" height="25" x="10" y="45" />
</g>
</defs>
<use xlink:href="#rects" x="10" y="10" stroke="black" fill="none" />
<use xlink:href="#rects" x="10" y="100" stroke="black" fill="none" />
<use xlink:href="#rects" x="10" y="190" stroke="black" fill="none" />
</svg>

Can I add a mask to an svg-element without using an id?

I want to assign a svg-mask to a svg-image. I can make this work using an id on the mask like this:
<svg id="svg1" width="5cm" height="5cm" viewBox="0 0 200 200"
xmlns="http://www.w3.org/2000/svg" version="1.1">
<defs>
<mask id="mask">
<circle cx="100" cy="100" r="100" fill="white"></circle>
</mask>
</defs>
<rect x="0" y="0" width="200" height="200" fill="red" mask="url(#mask)"></rect>
</svg>
However I want to load this svg multiple times, with a different id in the svg-tag. Therefore I will generate duplicates of the '#mask'-id. Using multiple id's is invalid code. So I want to use a class to refer to the appropriate mask. That means I cannot use the mask=url()-technique.
<svg id="svg2" width="5cm" height="5cm" viewBox="0 0 200 200"
xmlns="http://www.w3.org/2000/svg" version="1.1">
<defs>
<mask class="mask">
<circle cx="100" cy="100" r="100" fill="white"></circle>
</mask>
</defs>
<rect x="0" y="0" width="200" height="200" fill="red" mask="url(can't use this)"></rect>
</svg>
Is there a way I can apply a mask to the rect element if the mask has a class instead of id? Maybe using javaScript or some other way I didn't think of.
The full story/context:
I am actually making an svg image slider-module for Joomla with php. This php generates a module containing javascript, css and an svg. I use the javascript to animate the mask.
I do actually have it working with unique id's. I was just wondering if there is a way to assign a mask to an element without referring to id's. I may want to do this because my code is getting a bit more confusing to read, because I have to use some php in my javascript/svg and css for each unique id.
No. You can only reference masks via an id. You cannot reference SVG masks any other way.
According to your description I understand you have a identical grafical entity you want to mask with different forms, multiple times. Write that down DRY:
<!-- start with an invisible svg that only contains mask definitions -->
<svg width="0" height="0"
xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- first, you have a circular mask -->
<mask id="circle-mask">
<circle cx="100" cy="100" r="80" fill="white" />
</mask>
<!-- then, you have a different mask, lets say a diamond -->
<mask id="diamond-mask">
<polygon points="100,20 180,100 100,180 20,100" fill="white" />
</mask>
</defs>
</svg>
<!-- further into your document, you want to mask a rectangle -->
<svg id="svg1" width="5cm" height="5cm" viewBox="0 0 200 200"
xmlns="http://www.w3.org/2000/svg">
<!-- reference the circle mask -->
<rect x="0" y="0" width="200" height="200" fill="red" mask="url(#circle-mask)" />
</svg>
<!-- with the circle again, as often as you want, nothing changes -->
<svg id="svg2" width="5cm" height="5cm" viewBox="0 0 200 200"
xmlns="http://www.w3.org/2000/svg">
<!-- the mask is the same, so no difference to above -->
<rect x="0" y="0" width="200" height="200" fill="red" mask="url(#circle-mask)" />
</svg>
<!-- and now with the diamond; that one is different -->
<svg id="svg3" width="5cm" height="5cm" viewBox="0 0 200 200"
xmlns="http://www.w3.org/2000/svg">
<!-- if the mask changes, you need to change the reference -->
<rect x="0" y="0" width="200" height="200" fill="red" mask="url(#diamond-mask)" />
</svg>
You could also reference the masks in a stylesheet and give your referencing elements a class according to the mask shape:
.masked.circular rect {
mask: url(#circle-mask);
}
.masked.diamond rect {
mask: url(#diamond-mask);
}
<svg width="0" height="0"
xmlns="http://www.w3.org/2000/svg">
<defs>
<mask id="circle-mask">
<circle cx="100" cy="100" r="80" fill="white" />
</mask>
<mask id="diamond-mask">
<polygon points="100,20 180,100 100,180 20,100" fill="white" />
</mask>
</defs>
</svg>
<svg id="svg1" class="masked circular" width="5cm" height="5cm" viewBox="0 0 200 200"
xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" width="200" height="200" fill="red" />
</svg>
<svg id="svg2" class="masked circular" width="5cm" height="5cm" viewBox="0 0 200 200"
xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" width="200" height="200" fill="red" />
</svg>
<svg id="svg1" class="masked diamond" width="5cm" height="5cm" viewBox="0 0 200 200"
xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" width="200" height="200" fill="red" />
</svg>

ScrollMagic, reverse z-index order

I'm using scrollmagic to tween svg clippaths as you scroll. Usually with svgs(or divs or sections or whatever) the last thing you specify in your html is on top in terms of z-index, but you change that in css. I tried to do this with my svgs, so that while the first svg tweens, the other one scrolls up behind it. It seems like scrollmagic is preventing my z-indexing from working though. Any ideas?
http://codepen.io/kathryncrawford/pen/BoXOMJ
<div id="scene">
<svg id="svg1" height="500" width="800">
<image id="img1" xlink:href="http://placecage.com/800/500" x="0" y="0" width="800" height="500"/>
<defs>
<clipPath id="clip1">
<circle id="circle1" stroke="#000000" stroke-miterlimit="10" cx="400" cy="300" r="300" />
</clipPath>
</defs>
</svg>
</div>
<div id="scene2">
<svg id="svg2" height="500" width="800">
<image id="img2" xlink:href="http://fillmurray.com/800/500" x="0" y="0" width="800" height="500"/>
<defs>
<clipPath id="clip2">
<circle id="circle2" stroke="#000000" stroke-miterlimit="10" cx="400" cy="300" r="300" />
</clipPath>
</defs>
</svg>
</div>
CSS
#img1 {
clip-path: url(#clip1);
}
#img2 {
clip-path: url(#clip2);
}
#svg1, #circle1{
z-index: 2;
}
#svg2, #circle2{
z-index: 1;
}
Crap, I literally just figured it out after posting this. I changed the z-index css declaration to the z-index on the scene divs that wrap the svgs. That worked.
http://codepen.io/kathryncrawford/pen/BoXOMJ
#scene{
z-index: 2;
}
#scene2{
z-index: 1;
}

Categories