0

-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 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>';

    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);
8
  • I don't see any code where you draw the image multiple times. that's where this would happen. Each image gets the classes assigned at that time. It might be easier to draw the image with the canvas API to achieve your goal. Commented Nov 16, 2021 at 23:54
  • Also, do the Vectors have to be embedded into the canvas (as Rasters)? Commented Nov 16, 2021 at 23:57
  • Yeah I didn't want to overload the Question with too much code - but I'll add it in now.
    – Sirab33
    Commented Nov 17, 2021 at 0:29
  • @RandyCasburn OK, I've updated the question and added the code.
    – Sirab33
    Commented Nov 17, 2021 at 0:35
  • Just got back around to this. Check out the answer - let me know your questions. Commented Nov 17, 2021 at 4:30

2 Answers 2

2

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>

3
  • Wow - just waking up and seeing both your answer and @@RandyCasburn 's answer. Can't wait to try this - but have a full day of work ahead of me. Maybe I'll sneak this in during lunch? Will let you know later tonight!
    – Sirab33
    Commented Nov 17, 2021 at 13:49
  • I ended up trying this answer first, and it worked, so I'm marking it as correct answer. But I'm also going to try the 2nd answer and see how they differ. Wish there was a way of marking both as right answer!
    – Sirab33
    Commented Nov 18, 2021 at 15:31
  • I have a follow-up question to this one - would really appreciate it if you could chime in on it: stackoverflow.com/questions/70553339/…
    – Sirab33
    Commented Jan 2, 2022 at 3:16
1

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>

1
  • So like I wrote in my comment to @PaulLebeau 's answer, I'm just waking up and seeing both your answers - and I can't wait to try this - but have a full day of work ahead of me. Maybe I'll sneak this in during lunch? Will let you know later tonight!
    – Sirab33
    Commented Nov 17, 2021 at 13:50

Not the answer you're looking for? Browse other questions tagged or ask your own question.