Sunday, 11 January 2026

Web Tutorial: SVG Maker

One of the many annoying things about creating SVGs is the constant switching back-and-forth between code and on-screen rendering. One good way around this is multiple monitors, but that still requires moving the mouse cursor from one screen to the other. Why is it a big deal? Developers switch screens all the time when coding. That's true, but when you're dealing with SVGs, especially when writing paths, this gets repeated a lot and can be a huge pain in the ass.

The other way is to use a site like JsFiddle. It comes with several features which can be nice... but I'm a big fan of keeping things simple.

With that in mind, today we'll be using jQuery to create a quick-and-dirty application that I'm going to call the SVG Maker. The user will fill in things like width, height, and the inner XML, and the preview will show accordingly.

We begin with some standard HTML, and linkage to jQuery. In styling, we set the font.
<!DOCTYPE html>
<html>
  <head>
    <title>SVG Maker</title>

    <style>
      body
      {
        font-family: arial;
        font-size: 14px;
      }
    </style>

    <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>

    <script>

    </script>
  </head>

  <body>

  </body>
</html>


In the body, we add two divs, each styled using the CSS class panel, which we will create later.
<!DOCTYPE html>
<html>
  <head>
    <title>SVG Maker</title>

    <style>
      body
      {
        font-family: arial;
        font-size: 14px;
      }
    </style>

    <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>

    <script>

    </script>
  </head>

  <body>
    <div class="panel">
  
    </div>

    <div class="panel">

    </div>

  </body>
</html>


This will be followed by the addition of labels and inputs. We have txtWidth and txtHeight, which accept only numerical values above 10, with a default of 100. HTML5 is just wonderful, eh? We also have a textbox txtCode. Finally, we have a button, btnPreview.
<html>
  <head>
    <title>SVG Maker</title>

    <style>
      body
      {
         font-family: arial;
         font-size: 14px;
      }
    </style>

    <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>

    <script>

    </script>
  </head>

  <body>
    <div class="panel">
      <label for="txtWidth">Width:</label><input type="number" id="txtWidth" min="10" value="100" />
      <br />
      <label for="txtHeight">Height:</label><input type="number" id="txtHeight" min="10" value="100" />
      <br />
      <label for="txtCode">Code:</label><textarea cols="100" rows="10" id="txtCode"></textarea>
      <br />
      <button id="btnPreview">Preview</button>
      <br />

    </div>

    <div class="panel">

    </div>
  </body>
</html>


Perhaps most importantly, we have an SVG, svgResult. It has a black outline for visibility.
<html>
  <head>
    <title>SVG Maker</title>

    <style>
      body
      {
         font-family: arial;
         font-size: 14px;
      }
    </style>

    <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>

    <script>

    </script>
  </head>

  <body>
    <div class="panel">
      <label for="txtWidth">Width:</label><input type="number" id="txtWidth" min="10" value="100" />
      <br />
      <label for="txtHeight">Height:</label><input type="number" id="txtHeight" min="10" value="100" />
      <br />
      <label for="txtCode">Code:</label><textarea cols="100" rows="10" id="txtCode"></textarea>
      <br />
      <button id="btnPreview">Preview</button>
      <br />
    </div>

    <div class="panel">
      <svg id="svgResult" style="outline: 1px solid rgb(0, 0, 0)" xmlns="http://www.w3.org/2000/svg" />
    </div>
  </body>
</html>


Here it is. Nothing to shout about, eh? That will be improved on, soon.


Here's some CSS. panel gives the dashboard and preview area some definition with a nice orange outline. Bear in mind that this isn't meant to look pretty. It's meant to be useful visually, which isn't necessarily (and quite often isn't) the same thing.
<style>
  body
  {
     font-family: arial;
     font-size: 14px;
  }

  .panel
  {
     outline: 2px solid rgb(200, 100, 0);
     width: 90%;
     padding: 1em;
     margin: 1em auto 0 auto;
  }
</style>


We'll then style labels, inputs and buttons. Here, I set the display properties to inline-block and set width and float properties, so as to align them properly. It's not that important from an aesthetic point of view because it still doesn't look pretty, but at least it doesn't look like a mess, which is important from a usability point of view.
<style>
  body
  {
      font-family: arial;
      font-size: 14px;
  }

  .panel
  {
      outline: 2px solid rgb(200, 100, 0);
      width: 90%;
      padding: 1em;
      margin: 1em auto 0 auto;
  }

  label
  {
      display: inline-block;
      width: 10em;
      float: left;
  }

  input[type="number"]
  {
      width: 3em;
  }

  button
  {
      display: inline-block;
      width: 10em;
      float: right;
  }

</style>


And that's how it all looks like now.


We'll want some JavaScript here. Specifically, create the renderSVG() function.
<script>
  function renderSVG()
  {

  }

</script>


We want this function to run when the page loads.
<script>
  function renderSVG()
  {

  }

  $(document).ready(function() {
    renderSVG();
  });

</script>


And we also want to bind the execution to the click event of btnPreview, when the page loads.
<script>
  function renderSVG()
  {

  }

  $(document).ready(function() {
    renderSVG();

    $("#btnPreview").click(function() {
      renderSVG();
    });

  });
</script>


Now it's time to work on renderSVG(). Declare width, height and content. Set them to the values of these HTML elements. In the case of width and height, we want to coerce these values into numerical ones, using the parseInt() function.
function renderSVG()
{
  var width = parseInt($("#txtWidth").val());
  var height = parseInt($("#txtHeight").val());
  var content = $("#txtCode").val();

}


Here, we ensure that svgResult's width and height attributes match the values of width and height.
function renderSVG()
{
  var width = parseInt($("#txtWidth").val());
  var height = parseInt($("#txtHeight").val());
  var content = $("#txtCode").val();

  $("#svgResult").attr("width", width);
  $("#svgResult").attr("height", height);

}


Then we make sure that svgResult's HTML content is content. But we can't do it directly because it's an SVG. We have to first replace its HTML content with its HTML content, if that makes sense, then set it using content.
function renderSVG()
{
  var width = parseInt($("#txtWidth").val());
  var height = parseInt($("#txtHeight").val());
  var content = $("#txtCode").val();

  $("#svgResult").attr("width", width);
  $("#svgResult").attr("height", height);
  $("#svgResult").html($("#svgResult").html()).html(content);
}


Take a gander. When you reload the page now, the SVG is 100 by 100 pixels because those are the default values.


Change the number, click the Preview button, and watch the SVG change in size.


Add in some XML content, maybe a nice orange circle. Click Preview, and presto!


Adding a grid

It would be helpful to have some visual aids while creating the SVG. To that end, we'll add a customizable grid.

In the UI, we have txtGrid, a numerical input with a minimum value of 10. This means that the smallest grid we can have here is 10 by 10 pixels.
<label for="txtWidth">Width:</label><input type="number" id="txtWidth" min="10" value="100" />
<br />
<label for="txtHeight">Height:</label><input type="number" id="txtHeight" min="10" value="100" />
<br />
<label for="txtGrid">Grid:</label><input type="number" id="txtGrid" min="0" value="10" />
<br />

<label for="txtCode">Code:</label><textarea cols="100" rows="10" id="txtCode"></textarea>
<br />


And here's the new input.


In the JavaScript function renderSVG(), we handle this inclusion by declaring grid, and setting it to the value of the HTML element txtGrid. We also declare gridLines as an empty string. This will eventually be XML content. To that end, we will want to prepend gridlines to content when rendering the SVG.
function renderSVG()
{
  var width = parseInt($("#txtWidth").val());
  var height = parseInt($("#txtHeight").val());
  var grid = parseInt($("#txtGrid").val());
  var content = $("#txtCode").val();
  var gridLines = "";

  $("#svgResult").attr("width", width);
  $("#svgResult").attr("height", height);
  $("#svgResult").html($("#svgResult").html()).html(gridLines + content);
}


We only want to create the grid if gridLines is greater than 0.
function renderSVG()
{
  var width = parseInt($("#txtWidth").val());
  var height = parseInt($("#txtHeight").val());
  var grid = parseInt($("#txtGrid").val());
  var content = $("#txtCode").val();
  var gridLines = "";

  if (grid > 0)
  {

  }


  $("#svgResult").attr("width", width);
  $("#svgResult").attr("height", height);
  $("#svgResult").html($("#svgResult").html()).html(gridLines + content);
}

We'll then use a For loop to iterate from grid to width, using the value of grid as an incrementor...
function renderSVG()
{
  var width = parseInt($("#txtWidth").val());
  var height = parseInt($("#txtHeight").val());
  var grid = parseInt($("#txtGrid").val());
  var content = $("#txtCode").val();
  var gridLines = "";

  if (grid > 0)
  {
    for (var i = grid; i < width; i += grid)
    {

    }

  }

  $("#svgResult").attr("width", width);
  $("#svgResult").attr("height", height);
  $("#svgResult").html($("#svgResult").html()).html(gridLines + content);
}

...and with each iteration, use i to add the XML for vertical line tags to gridLines.
function renderSVG()
{
  var width = parseInt($("#txtWidth").val());
  var height = parseInt($("#txtHeight").val());
  var grid = parseInt($("#txtGrid").val());
  var content = $("#txtCode").val();
  var gridLines = "";

  if (grid > 0)
  {
    for (var i = grid; i < width; i += grid)
    {
      gridLines += '<line x1="' + i + '" y1="0" x2="' + i + '" y2="' + height + '" stroke="rgb(200,200,200)" stroke-width="1" />';
    }
  }

  $("#svgResult").attr("width", width);
  $("#svgResult").attr("height", height);
  $("#svgResult").html($("#svgResult").html()).html(gridLines + content);
}


Look at this! Grey vertical lines.

Then we repeat this, using height this time, for horizontal line tags.
function renderSVG()
{
  var width = parseInt($("#txtWidth").val());
  var height = parseInt($("#txtHeight").val());
  var grid = parseInt($("#txtGrid").val());
  var content = $("#txtCode").val();
  var gridLines = "";

  if (grid > 0)
  {
    for (var i = grid; i < width; i += grid)
    {
      gridLines += '<line x1="' + i + '" y1="0" x2="' + i + '" y2="' + height + '" stroke="rgb(200,200,200)" stroke-width="1" />';
    }

    for (var i = grid; i <= height; i += grid)
    {
      gridLines += '<line x1="0" y1="' + i + '" x2="' + width + '" y2="' + i + '" stroke="rgb(200,200,200)" stroke-width="1" />';
    }

  }

  $("#svgResult").attr("width", width);
  $("#svgResult").attr("height", height);
  $("#svgResult").html($("#svgResult").html()).html(gridLines + content);
}


Now we have a grid!


Adding objects

Time for a little value-add! We have the bare minimum - an interface to change the size and content of the SVG, and a grid to help draw. But what would add even more value?

Well, the ability to add pre-determined objects such as circles and lines to the SVG.

For that, let's add a series of buttons to the interface. Also add a checkbox with a label, cbAppend.
<div class="panel">
  <label for="txtWidth">Width:</label><input type="number" id="txtWidth" min="10" value="100" />
  <br />
  <label for="txtHeight">Height:</label><input type="number" id="txtHeight" min="10" value="100" />
  <br />
  <label for="txtGrid">Grid:</label><input type="number" id="txtGrid" min="0" value="10" />
  <br />
  <label for="txtCode">Code:</label><textarea cols="100" rows="10" id="txtCode"></textarea>
  <br />
  <label for="cbAppend">Append?</label><input type="checkbox" id="cbAppend" />
  <br />
  <button>Add Line</button>
  <button>Add Rect</button>
  <button>Add Ellipse</button>
  <button>Add Circle</button>
  <button>Add Path</button>
  <button>Add Text</button>
  <br />
  <hr />

  <button id="btnPreview">Preview</button>
  <br />
</div>


See the buttons and checkbox? Good.


Add an event to the first button. It will be a function call to addNode(), with the argument "line".
<button onClick="addNode('line')">Add Line</button>
<button>Add Rect</button>
<button>Add Ellipse</button>
<button>Add Circle</button>
<button>Add Path</button>
<button>Add Text</button>


In the JavaScript, create two functions, addNode() and getDefaultNode(). Both of them have the parameter nodeType.
function renderSVG()
{
  var width = parseInt($("#txtWidth").val());
  var height = parseInt($("#txtHeight").val());
  var grid = parseInt($("#txtGrid").val());
  var content = $("#txtCode").val();
  var gridLines = "";

  if (grid > 0)
  {
    for (var i = grid; i < width; i += grid)
    {
      gridLines += '<line x1="' + i + '" y1="0" x2="' + i + '" y2="' + height + '" stroke="rgb(200,200,200)" stroke-width="1" />';
    }

    for (var i = grid; i <= height; i += grid)
    {
      gridLines += '<line x1="0" y1="' + i + '" x2="' + width + '" y2="' + i + '" stroke="rgb(200,200,200)" stroke-width="1" />';
    }
  }

  $("#svgResult").attr("width", width);
  $("#svgResult").attr("height", height);
  $("#svgResult").html($("#svgResult").html()).html(gridLines + content);
}

function getDefaultNode(nodeType)
{

}

function addNode(nodeType)
{

}


$(document).ready(function() {
  renderSVG();

  $("#btnPreview").click(function() {
    renderSVG();
  });
});


Just for clarity, let's start on getDefaultNode(). We use a Switch statement on nodeType, because there are many possible values. For now, there is only "line". Notice we don't use a break statement here because all these cases return a value.
function getDefaultNode(nodeType)
{
  switch (nodeType)
  {
    case "line":
    default: return "";
  }

}


In the case of "line", an orange line tag is returned.
function getDefaultNode(nodeType)
{
  switch (nodeType)
  {
    case "line": return '<line x1="0" y1="0" x2="300" y2="200" stroke="rgb(250,100,0)" stroke-width="2" />';
    default: return "";
  }
}


Now for addNode(). We declare node, and use getDefaultNode(), passing in nodeType as the argument, to get the XML value which we then assign to this variable. Then we declare content, which is the text content of txtCode, and append, which is a Boolean depending on whether cbAppend is checked.
function addNode(nodeType)
{
  var node = getDefaultNode(nodeType);
  var content = $("#txtCode").val();
  var append = $("#cbAppend").prop("checked");

}


If append is true, that means we want node to be appended to content. We'll set txtCode's text content as that. If not, then we prepend node to content.
function addNode(nodeType)
{
  var node = getDefaultNode(nodeType);
  var content = $("#txtCode").val();
  var append = $("#cbAppend").prop("checked");

  if (append)
  {
    $("#txtCode").val(content + node);
  }
  else
  {
    $("#txtCode").val(node + content);
  }

}


After that, the updated code should be in txtCode. We'll run renderSVG().
function addNode(nodeType)
{
  var node = getDefaultNode(nodeType);
  var content = $("#txtCode").val();
  var append = $("#cbAppend").prop("checked");

  if (append)
  {
    $("#txtCode").val(content + node);
  }
  else
  {
    $("#txtCode").val(node + content);
  }

  renderSVG();
}


See what happens when I click the Add Line button? The code appears in the text box and an orange line appears in the SVG!


You'll notice that right now, it's not immediately apparent whether the line tag is appended or prepended. Let's just add the rest of the code. These are all the other tags that can be added. You may want to add your own.
function getDefaultNode(nodeType)
{
  switch (nodeType)
  {
    case "line": return '<line x1="0" y1="0" x2="300" y2="200" stroke="rgb(250,100,0)" stroke-width="2" />';
    case "rect": return '<rect width="200" height="100" x="10" y="10" rx="20" ry="20" fill="rgb(250,100,0)" />';
    case "ellipse": return '<ellipse rx="100" ry="50" cx="120" cy="80" fill="rgb(250,100,0)" stroke="rgb(250,100,0)" stroke-width="3" />';          
    case "circle": return '<circle r="45" cx="50" cy="50" fill="rgb(250,100,0)" />';
    case "path": return '<path d="M150 5 L75 200 L225 200 Z" fill="none" stroke="rgb(250,100,0)" stroke-width="3" />';
    case "text": return '<text x="5" y="15" fill="rgb(250,100,0)">abc 123</text>';

    default: return "";
  }
}


In the HTML, add the function calls.
<button onClick="addNode('line')">Add Line</button>
<button onClick="addNode('rect')">Add Rect</button>
<button onClick="addNode('ellipse')">Add Ellipse</button>
<button onClick="addNode('circle')">Add Circle</button>
<button onClick="addNode('path')">Add Path</button>
<button onClick="addNode('text')">Add Text</button>


Now add a rect tag and then a line, by clicking the buttons. Note that the line is prepended to the rect because the checkbox is not checked.


Check the checkbox and add a path tag. There, now you see that the path is appended.


How useful is it?

Well. I think it's useful. And of course I would, I made it! I have consciously avoided adding too many features to it precisely because the objective was to keep the interface as simple as possible while still incorporating features that are frequently used. Obviously, this varies from individual to individual.

<text>Till next time!</text>
T___T

No comments:

Post a Comment