Saturday 26 November 2022

Spot The Bug: No Face Given!

Tonight's episode of Spot The Bug is in, and it will feature HTML, jQuery and SVG.

HTML never ceases to surprise me. Every time I think I have its number, it flips the script on me. This was one such time, when I was trying to wrangle some dynamic SVG in jQuery.

All these bugs...

Basically, I had an SVG template for a face, and what I was trying to do was dynamically, on a click of a button, superimpose facial features onto that blank face. Sounds simple enough, right?

Here's the code for the HTML. Note that the JavaScript file is separate.
index.html
<!DOCTYPE html>
<html>
    <head>
        <title>Facemaker</title>

        <script src="https://code.jquery.com/jquery-1.12.4.js"></script>
        <script src="js/face.js"></script>

        <script>
            $(document).ready
            (
                () =>
                {
                    generateFace();
                }
            );
        </script>
    </head>
    
    <body>
        <table>
            <tr>
                <td style="width: 200px">
                    <label for="ddlEyes">Eyes
                        <select id="ddlEyes" onchange="generateFace()">
                            <option value="shades">Shades</option>
                            <option value="staring">Staring</option>
                            <option value="shut">Shut</option>
                        </select>
                    </label>
                    <br /><br />
                    <label for="ddlMouth">Mouth
                        <select id="ddlMouth" onchange="generateFace()">
                            <option value="grim">Grim</option>
                            <option value="lipstick">Lipstick</option>
                            <option value="smile">Smile</option>
                        </select>
                    </label>
                    <br /><br />
                    <label for="ddlNose">Nose
                        <select id="ddlNose" onchange="generateFace()">
                            <option value="big">Big</option>
                            <option value="narrow" selected>Narrow</option>
                            <option value="small">Small</option>
                        </select>
                    </label>
                </td>

                <td style="width: 200px">
                    <svg id="svgFace" style="width:200px; height:200px; outline: 1px solid black">

                    </svg>
                </td>
            </tr>
        </table>
    </body>
</html>


And here's the JavaScript code.
js/face.js
function generateFace()
{
    var eyesHTML = generateEyes($("#ddlEyes").val());
    var mouthHTML = generateMouth($("#ddlMouth").val());
    var noseHTML = generateNose($("#ddlNose").val());

    $("#svgFace").html("");

    var faceHTML = $("<ellipse />");
    faceHTML.attr("cx", 100);
    faceHTML.attr("cy", 100);
    faceHTML.attr("rx", 60);
    faceHTML.attr("ry", 80);
    faceHTML.attr("fill", "rgba(255, 200, 0, 1)");

    $("#svgFace").append(faceHTML);
    $("#svgFace").append(noseHTML);
    $("#svgFace").append(eyesHTML);
    $("#svgFace").append(mouthHTML);
}

function generateEyes(val)
{
    if (val == "shades")
    {
        var path = $("<path />");
        path.attr("d", "M55,65 C65,90 75,90 100,70 C125,90 135,90 145,65 Z");
        path.attr("fill", "black");

        return path;
    }

    if (val == "staring")
    {
        var text = $("<text />");
        text.html("O&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;O");
        text.attr("x", 100);
        text.attr("y", 80);
        text.attr("width", 100);
        text.attr("style", "font: 16px sans-serif");
        text.attr("stroke", "black");
        text.attr("stroke-width", 2);
        text.attr("text-anchor", "middle");

        return text;
    }

    if (val == "shut")
    {
        var text = $("<text />");
        text.html("U&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;U");
        text.attr("x", 100);
        text.attr("y", 80);
        text.attr("width", 100);
        text.attr("style", "font: 16px sans-serif");        
        text.attr("stroke", "black");
        text.attr("stroke-width", 2);
        text.attr("text-anchor", "middle");

        return text;
    }
}

function generateMouth(val)
{
    if (val == "grim")
    {
        var path = $("<path />");
        path.attr("d", "M80,150 C90,140 110,140 120,150 ");
        path.attr("stroke", "black");
        path.attr("stroke-width", 2);
        path.attr("fill", "none");

        return path;
    }

    if (val == "lipstick")
    {
        var ellipse = $("<ellipse />");
        ellipse.attr("cx", 100);
        ellipse.attr("cy", 140);
        ellipse.attr("rx", 20);
        ellipse.attr("ry", 5);
        ellipse.attr("stroke", "red");
        ellipse.attr("stroke-width", 3);
        ellipse.attr("fill", "none");

        return ellipse;
    }        

    if (val == "smile")
    {
        var path = $("<path />");
        path.attr("d", "M60,120 C90,140 110,140 140,120 ");
        path.attr("stroke", "black");
        path.attr("stroke-width", 2);
        path.attr("fill", "none");

        return path;
    }    
}

function generateNose(val)
{
    if (val == "big")
    {
        var polyline = $("<polyline />");
        polyline.attr("points", "60,60,90,60,80,120,120,120,110,60,140,60");
        polyline.attr("stroke", "black");
        polyline.attr("stroke-width", 2);
        polyline.attr("fill", "none");

        return polyline;
    }

    if (val == "small")
    {
        var path = $("<path />");
        path.attr("d", "M90,110 C95,115 105,115 110,110");
        path.attr("stroke", "black");
        path.attr("stroke-width", 2);
        path.attr("fill", "none");

        return path;
    }

    if (val == "narrow")
    {
        var path = $("<path />");
        path.attr("d", "M60,60 C70,50 90,40 95,60 L100,110 L105,60 C110,40 130,50 140,60");
        path.attr("stroke", "black");
        path.attr("stroke-width", 2);
        path.attr("fill", "none");

        return path;
    }
}


What went wrong

When selecting different options, the code should have cleared the template, and then generated a different face depending on the options selected. In fact, the code should have generated the face upon loading, depending on the default options set.

Alas, nothing happened no matter what I clicked.




Why it went wrong

Apparently, the browser does not interpret SVG tags in exactly the same way it does HTML tags. So using the append() method on an SVG would technically put the appropriate tags within the SVG, but it would not be rendered!

See what I mean? Inspecting the code shows that the tags were inside the SVG, but the browser was not reflecting it.




How I fixed it

Ultimately, all I needed was to append this line at the end of the function. What it does, is ensure that svgFace populates its own HTML with... well, its own HTML.
    $("#svgFace").append(faceHTML);
    $("#svgFace").append(noseHTML);
    $("#svgFace").append(eyesHTML);
    $("#svgFace").append(mouthHTML);
    
    $("#svgFace").html($("#svgFace").html());
}


And there, it worked!








Moral of the story

SVG was a relatively late addition to the HTML5 specification, and as such certain things have not quite caught up. Thankfully, the solution was very simple (if not entirely intuitive).

Way to (f)ace this one,
T___T

No comments:

Post a Comment