在运行时以编程方式更改 SVG 类

Posted

技术标签:

【中文标题】在运行时以编程方式更改 SVG 类【英文标题】:Programmatically change SVG classes during runtime 【发布时间】:2021-12-28 00:18:04 【问题描述】:

-我想将 same SVG 多次绘制到 canvas 上,但每次我想以编程方式更改 SVG 中特定类的颜色。

例如下面这张房子的图片:

这所房子的 SVG 具有以下类:

<style>

  .window-class 
    fill: lime;
  

  .door-class 
    fill: blue;
  

  .house-class 
    fill: tan;
  

  .roof-class 
    fill: red;
  

</style>

如何以编程方式访问这些特定的样式类,以便为我绘制的每个新房子更改它们的颜色值?

我通过创建一个 Image 对象然后使用以下代码将该图像绘制到 canvas 上来构造 SVG

    // 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"   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"  /><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"  /><rect id="Window1" class="window-class" x="58.61" y="160.74"  /><rect id="Door" class="door-class" x="92.11" y="228.74"  /></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;
    

通常我可以通过以下方式获得我想要更改颜色的特定班级:

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

然后我会遍历那个nodeList,并在我发现使用这个window-class 设置样式的每个元素处,我会这样做:

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

但由于我是按照上面显示的方式创建图像,我不确定如何获得其SVG 的特定类别。

有什么想法吗?

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

更新:

有人指出,没有包含多次绘制图像/SVG的代码,所以在这里:

        // 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();

所以当我现在运行它时,控制台显示 nodeList 为空:

    nodeList LENGTH = 0

所以基本上这个说法是行不通的:

    let nodeList = document.getElementsByClassName(currentClassName);

【问题讨论】:

我没有看到您多次绘制图像的任何代码。这就是发生这种情况的地方。每个图像都获得当时分配的类。使用 canvas API 绘制图像可能更容易实现您的目标。 另外,向量是否必须嵌入到画布中(作为光栅)? 是的,我不想用太多代码使问题超载 - 但我现在会添加它。 @RandyCasburn 好的,我已经更新了问题并添加了代码。 刚刚回到这个问题。查看答案 - 让我知道您的问题。 【参考方案1】:

要操作房子的 DOM,SVG 必须在 DOM 中。因此,我将 SVG 包裹在 &lt;div&gt; 中并隐藏了 div。我已经把它放在屏幕外,但我可以用其他几种方式隐藏 div。

一旦你这样做了,你的下一个问题是你正在改变元素的fill,但这将被你的 SVG 中的 CSS 覆盖。所以你必须删除那些 CSS 样式。

第三,您正在创建画布对象,但并未将它们附加到 DOM。

你也得到一个错误,因为canvasX 没有初始化。加上 CSS 长度必须有单位。所以你需要newCanvas.style.left = canvasX + "px"等。

您还错误地查找了您的元素。 getElementsByClassName(".hose-class") 找不到任何东西。必须是getElementsByClassName(".hose-class")

最后,我重写了元素查找和颜色分配代码。我将每个配色方案捆绑到一组配色方案对象中。它使将类映射到颜色变得更加简单。

// 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"   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"  />
    <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"  />
  <rect id="Window1" class="window-class" x="58.61" y="160.74"  />
  <rect id="Door" class="door-class" x="92.11" y="228.74"  />
</svg>

</div>

【讨论】:

哇——刚醒来,看到你的答案和@@RandyCasburn 的答案。迫不及待想试试这个——但我还有一整天的工作要做。也许我会在午餐时偷偷溜进去?今晚晚些时候会通知你! 我最终先尝试了这个答案,它奏效了,所以我将其标记为正确答案。但我也将尝试第二个答案,看看它们有何不同。希望有一种方法可以将两者都标记为正确答案! 我有一个关于这个问题的后续问题 - 如果你能插话,我将不胜感激:***.com/questions/70553339/…【参考方案2】:

以下是产生所需结果的一种方法。

    下面的方法在 HTML 中使用 &lt;svg&gt; 元素作为模板。该模板被克隆,应用颜色,转换为图像并放置到每个有颜色的房子的画布中。 注意:SVG 的结构发生了变化。 class 属性被自定义 data- 属性 data-part 替换,该属性用于通过普通 CSS 选择器应用填充样式。 每个房子的坐标位置都在一个空格分隔的x y坐标数组中。该数组还指示要绘制的房屋数量 房屋“部分”的颜色包含在列出房屋“部分”及其相应颜色的对象中(颜色的数量应与房屋数量相匹配) 所有&lt;canvas&gt; CSS 都已移至样式表。

我会让你处理画布上图像的大小。

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"   viewBox="0 0 240.26 311.24"><defs></defs><g id="House"><rect data-part="House" x="30.08" y="131.74"  /><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"  /><rect data-part="Window"  x="58.61" y="160.74"  /><rect data-part="Door" x="92.11" y="228.74"  /></svg>
</body>
</html>

【讨论】:

所以就像我在对@PaulLebeau 的回答的评论中写的那样,我刚醒来,看到了你的两个答案——我迫不及待想试试这个——但是有一整天的时间在我前面工作。也许我会在午餐时偷偷溜进去?今晚晚些时候会通知你!

以上是关于在运行时以编程方式更改 SVG 类的主要内容,如果未能解决你的问题,请参考以下文章

无法在运行时以编程方式影响闪烁控制

在使用像 EF 这样的 ORM 时,是不是可以在运行时以编程方式创建新表?

我可以在应用程序运行时以编程方式翻转 Info.plist 值吗?

如何在运行 asp.net 页面时以编程方式设置表格背景?

如何在运行时以编程方式计算应用 ID?

如何在其他呼叫运行时以编程方式合并呼叫(电话会议)