Home > Blockchain >  Programmatically change SVG classes during runtime
Programmatically change SVG classes during runtime

Time:11-17

-I want to draw the same SVG onto a canvas multiple times, but each time I want to PROGRAMMATICALLY change the colors of specific classes within that SVG.

For example take this house image below:

enter image description here

The SVG for this House has the following classes:

<style>

  .window-class {
    fill: lime;
  }

  .door-class {
    fill: blue;
  }

  .house-class {
    fill: tan;
  }

  .roof-class {
    fill: red;
  }

</style>

How do I programmatically access these specific Style-Classes so I can change their color values for each new house that I draw?

I’m constructing the SVG by creating an Image object and then drawing that image onto a canvas using the following code:

    // 1. Create the CANVAS and the CONTEXT:
    var theCanvas = document.getElementById("theCanvas");
    var theContext = theCanvas.getContext("2d");
    theContext.fillStyle = "lightblue";
    theContext.fillRect(0, 0, theCanvas.width, theCanvas.height);

    // 2. Define the SVG data:
    var imageData = '<svg id="HOUSE" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="240.26" height="311.24" viewBox="0 0 240.26 311.24"><defs><style>.house-class {fill: tan;}.roof-class {fill: red;}.roof-class, .window-class, .door-class {stroke: #000;stroke-miterlimit: 10;}.window-class {fill: lime;}.door-class {fill: blue;}</style></defs><g id="House"><rect  x="30.08" y="131.74" width="173.07" height="179"/><path d="M270,242V420H98V242H270m1-1H97V421H271V241Z" transform="translate(-67.39 -109.76)"/></g><polygon id="Roof"  points="1.11 131.74 239.11 131.74 117.11 0.74 1.11 131.74"/><rect id="Window2"  x="145.11" y="160.74" width="30" height="42"/><rect id="Window1"  x="58.61" y="160.74" width="30" height="42"/><rect id="Door"  x="92.11" y="228.74" width="52" height="82"/></svg>';

    var DOMURL = window.URL || window.webkitURL || window;

    var img = new Image();
    var svg = new Blob([imageData], { type: 'image/svg xml;charset=utf-8' });
    var url = DOMURL.createObjectURL(svg);

    img.onload = function () {
        theContext.drawImage(img, 0, 0);
        DOMURL.revokeObjectURL(url);
    }

    img.src = url;
    

Ordinarily I'd be able to get at the specific Classes who's colors I want to change by using this:

    let nodeList = document.getElementsByClassName("window-class");

And then I would iterate through that nodeList and at each element I find that is styled with this window-class, I would do:

        element.style.fill = -whatever-the-next-color-would-be-;

But since I'm creating my image in the manner I showed above, I'm not sure how I can get at specific classes of its SVG.

Any thoughts?

==============================

UPDATE:

It was pointed out that the code for drawing the image/SVG multiple times wasn't included, so here it is:

        // GLOBAL VARIABLES:
        const TOTAL_IMAGES = 3;  // could be 30, or 300
        const canvasWidth = 250;
        const canvasHeight = 320;

        var canvasX, canvasY = 0;

        // COLOR VARIABLES:
        var colorCounter = 0;
        let houseColorsArray = ["fuchsia", "gold", "lighblue"]; // Will have lots more colors for all of these 
        let windowColorsArray = ["yellow", "pink", "lightgreen"];
        let roofColorsArray = ["maroon", "crimson", "darkred"];
        let doorColorsArray = ["darkBlue", "purple", "darkslategray"];


        // CLASS-NAMES
        let classNamesToPaintArray = [".house-class", ".door-class", ".window-class", ".roof-class"];



        function designOneHouse(theCanvas) {
            console.log("\n\n==========================\n=");
            console.log("= =>>In 'designOneHouse()'!\n");

            // 1. Create a Color-Scheme:
            let houseColor = houseColorsArray[colorCounter];
            let doorColor = doorColorsArray[colorCounter];
            let windowColor = windowColorsArray[colorCounter];
            let roofColor = roofColorsArray[colorCounter];
            console.log("  ->Current 'houseColor' = ", houseColor);
            console.log("  ->Current 'doorColor' = ", doorColor);
            console.log("  ->Current 'windowColor' = ", windowColor);
            console.log("  ->Current 'roofColor' = ", roofColor);

            let context = theCanvas.getContext("2d");


            // Iterate the ColorCounter - making sure we don't overflow the ColorsArrays:
            colorCounter  ;
            if(colorCounter == houseColorsArray.length) {
                colorCounter = 0;
            }


            // Now GET-AT and PAINT the Individual SVG Components.
            // STRATEGY:
            // 1. Iterate through the Array containing all the CLASS-NAMES who's color I want to change.
            // 2. For each of these classes, I'll need to iterate through all the HTML elements that are OF that class type
            //    (there may be like 10 elements that are all styled by the same Style; I want all of them to be updated!)
            // 

            for(classNameCounter = 0; classNameCounter < classNamesToPaintArray.length; classNameCounter  ) {
                let currentClassName = classNamesToPaintArray[classNameCounter];
                console.log("currentClassName = "   currentClassName);

                let nodeList = document.getElementsByClassName(currentClassName);
                console.log("nodeList = "   nodeList);
                console.log("nodeList LENGTH = "   nodeList.length);

                for(var counter = 0; counter < nodeList.length; counter  ) {
                    console.log("\n\n===>>IN FOR LOOP -- Node-COUNTER = "   counter);
                    let currentNode = nodeList[counter];
                    console.dir("  > 'childNodes[0]' of 'currentNode' = ");
                    console.dir(currentNode.childNodes[0]);

                    let elements = document.querySelectorAll(".door-class");
                    // Change the text of multiple elements with a loop
                    elements.forEach(element => {
                        element.style.fill = "pink";
                    });

                }

            }

        }



        function makeCanvasGrid() {
            console.log("\n\n====>In 'makeCanvasGrid()'!\n");

            for(var canvasCounter = 0; canvasCounter < TOTAL_IMAGES; canvasCounter  ) {
                console.log("\n >FOR LOOP - canvasCounter = "   canvasCounter);

                // 1. Create a new Canvas Object:
                let newCanvas = document.createElement("canvas");
                newCanvas.setAttribute("width", canvasWidth);
                newCanvas.setAttribute("height", canvasHeight);
                newCanvas.setAttribute("id", "newCanvas"   canvasCounter);
                // Log-out just to verify the "id" property was set correctly:
                console.log("  >newCanvas.id  = "   newCanvas.id);

                // 2. Place the Canvas at (x,y) (top, left) coordinates:
                newCanvas.style.position = "absolute";
                newCanvas.style.left = canvasX; //"100px";
                newCanvas.style.top = canvasY;  //"100px";

                designOneHouse(newCanvas);


                // Check the current Canvas' (X, Y) coords, and if needed, reset X to 0 and SKIP to the next "ROW" of Canvasses:
                if(canvasCounter > 0 && canvasCounter % 3 == 0) {
                    console.log("  >>NEXT ROW PLEASE!!!! canvasCount = ", canvasCounter);
                    canvasX = 0;
                    canvasY  = canvasHeight   20;
                }
                else {
                    canvasX  = canvasWidth   10;
                }
            }
        }


        makeCanvasGrid();

SO when I run this right now, the console shows me the nodeList is empty:

    nodeList LENGTH = 0

So basically this statement isn't working:

    let nodeList = document.getElementsByClassName(currentClassName);

CodePudding user response:

Below is one way to produce your desired result.

  1. The approach below has the <svg> element in the HTML to be used as a template. That template is cloned, colors applied, converted into an Image and placed into the canvas for each house that has colors.
    • Note: the structure of the SVG changed. The class attributes are replaced by a custom data- attribute data-part that is used to apply the fill style through a normal CSS selector.
  2. The coordinate positions of each house are in an array of space separated x y coordinates. The array also indicates how many houses are to be drawn
  3. The colors for the house 'parts' are included in an Object that lists the house 'part' and its corresponding color (the count of colors should match the number of houses)
  4. All <canvas> CSS is moved to the stylesheet.

I'll let you deal with sizing the image on the canvas.

const canvas = document.querySelector('canvas');
const context = canvas.getContext("2d");

const housePositions = ["0 10", "85 10", "170 10"];
const parts = {
  House: ["fuchsia", "gold", "lightblue"],
  Window: ["yellow", "pink", "lightgreen"],
  Roof: ["maroon", "crimson", "darkred"],
  Door: ["darkBlue", "purple", "darkslategray"]
};

function addHouse(colorIndex, x, y) {
  let clonedSvgElement = document.querySelector('svg').cloneNode(true);
  Object.keys(parts)
    .forEach(part => {
      clonedSvgElement.querySelectorAll(`[data-part=${part}]`)
        .forEach(item => {
          item.style.fill = parts[part][colorIndex];
        });
      const blob = new Blob([clonedSvgElement.outerHTML], { type: 'image/svg xml;charset=utf-8' });
      const blobURL = URL.createObjectURL(blob);
      const image = new Image();
      image.onload = () => {
        context.drawImage(image, x, y, 130, 110);
        URL.revokeObjectURL(this.src);
      };
      image.src = blobURL;
    });
}

housePositions.forEach((coordString, index) => {
  const [x, y] = coordString.split(' ');
  addHouse(index, x, y);
});
canvas {
    position: absolute;
    left: 10px;
    top: 10px;
    width: 150px;
    height: 80px;
    border: 1px solid;
    background-color: lightblue;
}

svg {
    display: none;
}
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="stylesheet" href="index.css">

    <title>Document</title>
    <script defer src="index.js"></script>
</head>
<body>
<canvas></canvas>
<svg id="HOUSE" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="140" height="140" viewBox="0 0 240.26 311.24"><defs></defs><g id="House"><rect data-part="House" x="30.08" y="131.74" width="173.07" height="179"/><path d="M270,242V420H98V242H270m1-1H97V421H271V241Z" transform="translate(-67.39 -109.76)"/></g><polygon data-part="Roof" points="1.11 131.74 239.11 131.74 117.11 0.74 1.11 131.74"/><rect data-part="Window" x="145.11" y="160.74" width="30" height="42"/><rect data-part="Window"  x="58.61" y="160.74" width="30" height="42"/><rect data-part="Door" x="92.11" y="228.74" width="52" height="82"/></svg>
</body>
</html>
<iframe name="sif1" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>

CodePudding user response:

To manipulate the house's DOM, the SVG has to be in the DOM. So I've wrapped the SVG in a <div> and hidden the div. I've put it way offscreen, but I could have hidden the div in several other ways.

Once you do that, your next problem is that you are changing the fill of the elements, but that will be overridded by the CSS in your SVG. So you have to remove those CSS styles.

Thirdly, you are creating canvas objects, but you are not attaching them to the DOM.

Also you are getting an error because canvasX isn't initialised. Plus CSS lengths must have units. So you need newCanvas.style.left = canvasX "px" etc.

You were also looking up your elements wrongly. getElementsByClassName(".hose-class") won't find anything. It needed to be getElementsByClassName(".hose-class").

Finally, I've rewritten the element lookup and colour assignment code. I've bundled each colour scheme up into an array of colour scheme objects. It makes mapping classes to colours much simpler.

// GLOBAL VARIABLES:
const TOTAL_IMAGES = 3;  // could be 30, or 300
const canvasWidth = 250;
const canvasHeight = 320;

var canvasX = 0, canvasY = 0;

// COLOR VARIABLES:
var colorCounter = 0;

let houseColorSchemes = [ {".house-class": "fuchsia",
                           ".door-class": "darkblue",
                           ".window-class": "yellow",
                           ".roof-class": "maroon"},
                     
                          {".house-class": "gold",
                           ".door-class": "purple",
                           ".window-class": "pink",
                           ".roof-class": "crimson"},
                     
                          {".house-class": "lightblue",
                           ".door-class": "darkslategray",
                           ".window-class": "lightgreen",
                           ".roof-class": "darkred"} ];
                   

// CLASS-NAMES
let classNamesToPaintArray = [".house-class", ".door-class", ".window-class", ".roof-class"];

// SVG template
let houseSVG = document.getElementById("HOUSE");


function designOneHouse(theCanvas) {
  console.log("\n\n==========================\n=");
  console.log("= =>>In 'designOneHouse()'!\n");

  let context = theCanvas.getContext("2d");

  // Now GET-AT and PAINT the Individual SVG Components.
  // STRATEGY:
  // 1. Iterate through the Array containing all the CLASS-NAMES who's color I want to change.
  // 2. For each of these classes, I'll need to iterate through all the HTML elements that are OF that class type
  //    (there may be like 10 elements that are all styled by the same Style; I want all of them to be updated!)
  // 

  let colorScheme = houseColorSchemes[colorCounter];
  
  classNamesToPaintArray.forEach(className => {
    let elements = houseSVG.querySelectorAll(className);
    
    elements.forEach(element => element.style.fill = colorScheme[className]);
  });
  

  var imageData = houseSVG.outerHTML;

  var DOMURL = window.URL || window.webkitURL || window;

  var img = new Image();
  var svg = new Blob([imageData], { type: 'image/svg xml;charset=utf-8' });
  var url = DOMURL.createObjectURL(svg);

  img.onload = function () {
    context.drawImage(img, 0, 0);
    DOMURL.revokeObjectURL(url);
  }

  img.src = url;


  // Iterate the ColorCounter - making sure we don't overflow the ColorsArrays:
  colorCounter  ;
  if(colorCounter == houseColorSchemes.length) {
    colorCounter = 0;
  }


}



function makeCanvasGrid() {
  console.log("\n\n====>In 'makeCanvasGrid()'!\n");

  for(var canvasCounter = 0; canvasCounter < TOTAL_IMAGES; canvasCounter  ) {
    console.log("\n >FOR LOOP - canvasCounter = "   canvasCounter);

    // 1. Create a new Canvas Object:
    let newCanvas = document.createElement("canvas");
    newCanvas.setAttribute("width", canvasWidth);
    newCanvas.setAttribute("height", canvasHeight);
    newCanvas.setAttribute("id", "newCanvas"   canvasCounter);
    // Log-out just to verify the "id" property was set correctly:
    console.log("  >newCanvas.id  = "   newCanvas.id);

    // 2. Place the Canvas at (x,y) (top, left) coordinates:
    newCanvas.style.position = "absolute";
    newCanvas.style.left = canvasX   "px"; //"100px";
    newCanvas.style.top = canvasY   "px";  //"100px";

    document.body.appendChild(newCanvas);

    designOneHouse(newCanvas);


    // Check the current Canvas' (X, Y) coords, and if needed, reset X to 0 and SKIP to the next "ROW" of Canvasses:
    if(canvasCounter > 0 && canvasCounter % 3 == 0) {
      console.log("  >>NEXT ROW PLEASE!!!! canvasCount = ", canvasCounter);
      canvasX = 0;
      canvasY  = canvasHeight   20;
    }
    else {
      canvasX  = canvasWidth   10;
    }
  }
}


makeCanvasGrid();
#house-template {
  position: absolute;
  left: -1000px;
}
<div id="house-template">

<svg id="HOUSE" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="240.26" height="311.24" viewBox="0 0 240.26 311.24">
  <defs>
    <style>
      .roof-class, .window-class, .door-class {stroke: #000;stroke-miterlimit: 10;}
    </style>
  </defs>
  <g id="House">
    <rect class="house-class" x="30.08" y="131.74" width="173.07" height="179"/>
    <path d="M270,242V420H98V242H270m1-1H97V421H271V241Z" transform="translate(-67.39 -109.76)"/>
  </g>
  <polygon id="Roof" class="roof-class" points="1.11 131.74 239.11 131.74 117.11 0.74 1.11 131.74"/>
  <rect id="Window2" class="window-class" x="145.11" y="160.74" width="30" height="42"/>
  <rect id="Window1" class="window-class" x="58.61" y="160.74" width="30" height="42"/>
  <rect id="Door" class="door-class" x="92.11" y="228.74" width="52" height="82"/>
</svg>

</div>
<iframe name="sif2" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>

  • Related