Home > Software engineering >  Changing SVG components in javascript
Changing SVG components in javascript

Time:10-28

I have the following code in a tiny web page:

<svg width='200' height='200'>
  <svg x=0 y=0>
    <circle cx=50 cy=50 r=40 stroke='#808080' stroke-width=3 fill='#FF0000'/>
    <circle cx=150 cy=50 r=40 stroke='#808080' stroke-width=3 fill='#00FF00'/>
    <circle cx=50 cy=150 r=40 stroke='#808080' stroke-width=3 fill='#0000FF'/>
    <circle cx=150 cy=150 r=40 stroke='#808080' stroke-width=3 fill='#FFFF00'/>
  </svg>
  <svg id='CtrBtn' x=0 y=0>
    <circle cx=100 cy=100 r=20 stroke='#808080' stroke-width=3 fill='#000000'/>
  </svg>
</svg>

<div id='status'>STATUS</div>

<script type='text/javascript'>
window.onload = btnHandler

function btnHandler()   {
  let divCtrBtn = document.getElementById('CtrBtn')
  divCtrBtn.onclick = function()   {
    document.getElementById('status').innerHTML = 'Center-Button-Hit'
  }
}
</script>

It works as I expect showing this for start:

enter image description here

And then this once I click the black button in the middle:

enter image description here

But this is what I want instead when clicking the button:

The red disk should change color to become cyan (#00FFFF) and the green should change color to become magenta (#FF00FF).

How should I change the code of the function btnHandler() to get this result ?

CodePudding user response:

You need to target the fill attribute. In vanilla JS this is done with the setAttribute method. EDIT: You also need a way to target them, I've just given an id to each circle...

Just add those two extra lines in (see snippet below)

window.onload = btnHandler

function btnHandler()   {
  let divCtrBtn = document.getElementById('CtrBtn')
  divCtrBtn.onclick = function()   {
    document.getElementById('status').innerHTML = 'Center-Button-Hit'
    // extra lines here
    document.getElementById('circle1').setAttribute('fill', '#00FFFF')
    document.getElementById('circle2').setAttribute('fill', '#FF00FF')
  }
}
<svg width='200' height='200'>
  <svg x=0 y=0>
    <circle id="circle1" cx=50 cy=50 r=40 stroke='#808080' stroke-width=3 fill='#FF0000'/>
    <circle id="circle2" cx=150 cy=50 r=40 stroke='#808080' stroke-width=3 fill='#00FF00'/>
    <circle id="circle3" cx=50 cy=150 r=40 stroke='#808080' stroke-width=3 fill='#0000FF'/>
    <circle id="circle4" cx=150 cy=150 r=40 stroke='#808080' stroke-width=3 fill='#FFFF00'/>
  </svg>
  <svg id='CtrBtn' x=0 y=0>
    <circle id="centreCirle"cx=100 cy=100 r=20 stroke='#808080' stroke-width=3 fill='#000000'/>
  </svg>
</svg>

<div id='status'>STATUS</div>

CodePudding user response:

While you've already accepted an answer, I'll post an alternative that may also be useful to you along with explanatory comments in the JavaScript to explain how it works:

/* a relatively simple function to convert a string into chunks of size 'size': */
const chunk = (str, size) => {
    /* establishing how many chunks we need: */
    const parts = Math.ceil(str.length / size),
      // converting the string into an Array, using String.prototype.split()
      // with an empty-string passed as the argument:
      temp = str.split('');

    // we return the result of this expression; first we create a new Array
    // using Array.from():
    return Array.from({
      // defining the length of the created Array:
      length: parts
      // using Array.prototype.map() to iterate over the created Array, and
      // create a new array:
    }).map(
      // here we take a substring of Array-elements from the temp Array
      // using splice, to take the elements between index 0 and a length
      // represented by the size; we use Array.prototype.splice() because
      // that modifies the Array on which it operates, allowing us to take
      // from index 0 every iteration; we then join the array-elements
      // together using Array.prototype.join():
      () => temp.splice(0, size).join('')
    );
  },
  // a simple function to handle the colour-cycling, since your colour modifications
  // are basically an inversion, FF00FF to 00FF00 (as an example):
  cycleColor = (el) => {
    // here we get the current 'fill' attribute-value of the element, and replace
    // all characters in the attribute-value that are not in the hex range of
    // 0-f with an empty space; hence '#FF00FF' becomes 'FF00FF'; this string
    // is then passed to the chunk function, and split into an Array of parts
    // each a size of 2:
    let current = chunk(el.getAttribute('fill').replace(/([^0-9a-f])/ig, ''), 2),
      // the updated colour is created by iterating over the current colour and
      // uses Array.prototype.map() to create a new Array:
      updated = current
      .map(
        // color is a reference to the current hex colour, and from the
        // original string 'FF00FF' will become an Array of values ['FF', '00', 'FF']
        // each of those hex-values will be referenced in turn. Here we
        // subtract the parsed Integer of the color (from base 16) from
        // 255 (in the rgb colour-space 255 is the maximum value), and
        // we convert that back into a hexadecimal representation using
        // Number.prototype.toString(16):
        (color) => (255 - parseInt(color, 16)).toString(16))
      // because the above use of map allows for 0 to be returned this would
      // result in an invalid hex-value:
      .map((color) => {
        // here, if the length of the color is 2 ('FF' for example),
        // we return that color; otherwise (in the case of '0') we
        // pad the String by placing a 0 at the front of the string: 
        return color.length === 2 ? color : '0'   color;
      })
      // we then join the Array-elements together with an empty-string:
      .join('');
    // and then we use Element.setAttrbute() to update the property to
    // the new colour string, using a template-literal to concatenate
    // a '#' character:
    el.setAttribute('fill', `#${updated}`);
  },

  // defining your original function as an Arrow function:
  btnHandler = () => {
    let divCtrBtn = document.getElementById('CtrBtn')
    // revising your event-binding to use EventTarget.addEventListener()
    // to bind the anonymous function to the 'click' event fired on the
    // divCtrBtn element:
    divCtrBtn.addEventListener('click', (evt) => {
      // there was no obvious reason to add an attribute to identify the
      // elements to update, since CSS could already target them; here
      // we use document.querySelectorall() to find all <circle> elements
      // in the range of -n   2 (the first two elements) that are found
      // inside of an <svg> without an [id] attribute; we then use
      // NodeList.prototype.forEach() to iterate over the retrieved elements:
      document.querySelectorAll('svg:not([id]) > circle:nth-child(-n   2)').forEach(
        // and call the cycleColor function on each of them:
        (el) => cycleColor(el)
      );

      document.getElementById('status').innerHTML = 'Center-Button-Hit'
    });
  }

// as above, I changed the 'onload' event-binding to use the
// EventTarget.addEventListener() method, and call the btnHandler()
// function in response to the 'DOMContentLoaded' event:
window.addEventListener('DOMContentLoaded', (e) => {
  btnHandler();
});
<svg width='200' height='200'>
  <svg x=0 y=0>
    <circle cx=50 cy=50 r=40 stroke='#808080' stroke-width=3 fill='#FF0000' />
    <circle cx=150 cy=50 r=40 stroke='#808080' stroke-width=3 fill='#00FF00' />
    <circle cx=50 cy=150 r=40 stroke='#808080' stroke-width=3 fill='#0000FF' />
    <circle cx=150 cy=150 r=40 stroke='#808080' stroke-width=3 fill='#FFFF00' />
  </svg>
<svg id='CtrBtn' x=0 y=0>
    <circle cx=100 cy=100 r=20 stroke='#808080' stroke-width=3 fill='#000000' />
  </svg>
</svg>

<div id='status'>STATUS</div>

References:

  • Related