SVG Graphics 1
SVG Graphics 1
Using SVG graphics
SVG (scalable vector graphics) is a special kind of HTML element that is used to create an image object. Unlike the HTML5 canvas shapes, a SVG image is an actual object that can be assigned an id attribute. That makes SVG images useful for pages that use JavaScript, as this makes SVG images easy to select using JavaScript. Therefore, knowing some of the basics of SVG images is a good thing to know for a web programmer.
Starting out the lesson
Starting using CodeSandbox
Login to your CodeSandbox account. Select the Frontend for practicalcompute.cc Template. In the upper right corner, select the Fork drop-down and select your project area name. After the fork is created, go back to the Dashboard and rename this forked sandbox to svg_graphics1.
Starting using vite
Change into your ~/Documents folder and run degit to create a new project called svg_graphics1. Here are the commands to follow:
$ cd ~/Documents
$ npx degit takebayashiv-cmd/vite-template-my-project svg_graphics1
$ cd svg_graphics1
$ npm install
$ npm run dev
The last command will start the vite server. So, you can go to http://localhost:5173 to view the current app.
Creating JavaScript classes for SVG images
Let’s create a class for drawing a SVG rectangle. Start by deleting the myclasses.mjs file, and creating a new file called "svgshapes.mjs". If you are not using CodeSandBox, you can do this using Visual Studio Code. Here are the starting contents for svgshapes.mjs:
export class Rectangle {
constructor(x, y, width, height, color) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.color = color;
this.id = '';
}
draw(svg_area) {
const svgNS = "http://www.w3.org/2000/svg";
const rect = document.createElementNS(svgNS, "rect");
rect.setAttribute("x", this.x);
rect.setAttribute("y", this.y);
rect.setAttribute("width", this.width);
rect.setAttribute("height", this.height);
rect.setAttribute("fill", this.color);
rect.setAttribute("id", this.id);
svg_area.appendChild(rect);
}
}
As with the classes we created for the HTML5 canvas shapes, this Rectangle class consists of a constructor on lines 2-8, and a draw() method defined on lines 10-19. The constructor is used to set values for the (x, y) coordinates of the top left corner of the rectangle, the width and height of the rectangle and the fill color for the rectangle.
The draw() method has the parameter svg_area which is a reference to the <svg> container element used to hold all the SVG elements we want to display. Note that unlike creating regular HTML elements like a <tr> element, to create a SVG object you call document.createElementNS() (instead of document.createElement()). This is because the namespace must be specified when creating a SVG element.
Next we can modify index.mjs. Here is the new version of index.mjs
import { Rectangle } from "./svgshapes.mjs";
if (document.readyState === "loading") {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
function init() {
console.log('init called');
}
Line 1 has been changed to import from the svgshapes.mjs file. The init() function has some lines removed that referred to the Student class.
If you look at the Console, you should see 'init called'. Next, we modify index.html, so that it has a svg container element. We will also change some things like the title.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script type="module" src="index.mjs"></script>
<title>SVG Graphics 1</title>
</head>
<body>
<h1>Drawing area</h1>
<svg
id="svg_area"
width="400"
height="400"
xmlns="http://www.w3.org/2000/svg"
></svg>
</body>
</html>
The new lines are 6, 9 and 10-15. Line 6 changed the contents of the <title> element to "SVG Graphics 1". Line 9 changed the contents of the <h1> element to "Drawing area". Lines 10-15 define the svg element that will serve as the container for all the other SVG objects we want to draw. Note that this element has an id="svg_area". That will be used to get a reference to this SVG container element. The SVG container will have a width and height of 400 pixels, and uses the namespace for SVG elements.
Using the Rectangle class
Simple example
To show how the Rectangle class can be used, let’s modify index.mjs to just draw a single rectangle. Here is the new version of the code:
import { Rectangle } from "./svgshapes.mjs";
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
function init() {
console.log("init called");
const svg_area = document.getElementById("svg_area");
const rect1 = new Rectangle(50, 60, 70, 40, "red");
rect1.draw(svg_area);
}
The new lines are 11-13. Line 11 gets a reference to the SVG container area. Line 12 constructs a Rectangle object at (50, 60) with a width of 70 and a height of 40, and is colored red.
Here is a screen shot showing that rectangle in the browser window:
Outline steps to complete the app
We want to get more practice with repetition statements. So, we will use nested for loops to draw rectangles in the SVG container. A nested for loop consists of a for loop inside of another for loop. To get a feel for this, we can draw rows and columns of rectangles can help us to visualize nested for loops in action.
-
Input - We want to let the user draw some rows and columns of rectangles. So, we will need to gather input for how many rows and how many columns for each row. In addition, we want to use colors for the rectangles and let the user specify if the color for all the columns in a row is fixed, but the color for each row changes. Or, the user can specify that the color for each column in a row changes, but the next row will repeat that same sequence of column colors. So, either fixed color for row, or fixed color for column is what we want the user to specify. So, we will need input text boxes for the number of rows and number of columns. And, we want to use a select list to choose between fixed row color or fixed column color.
-
Repetition statements - We will use nested for loops to draw the rows and columns of rectangles. To clear the SVG area to redraw rectangles again, we need to use a while loop to remove all the SVG objects. Within the for loops we need to determine how much to increment each loop by to draw the rectangles without overlap. This will depend on the width and height of the rectangles we choose to use.
-
Selection statements - We need to be able to handle the two cases: fixed row color vs fixed column color.
-
Output - We want to display SVG rectangles in the SVG area.
Adding the markup to gather input
Let’s modify index.html so that we can gather the input that we need to draw the rows and columns of rectangles. Here is the new version of index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script type="module" src="index.mjs"></script>
<title>SVG Graphics 1</title>
</head>
<body>
<h1>Drawing area</h1>
<svg
id="svg_area"
width="400"
height="400"
xmlns="http://www.w3.org/2000/svg"
></svg>
<h2>Rectangles setup</h2>
Number of rows:
<input type="text" id="rows_box" /><br />
Number of columns:
<input type="text" id="columns_box" /><br />
Fixed color type:
<select id="fixed_type_select">
<option>Fixed row color</option>
<option>Fixed column color</option>
</select>
<br /><br />
<button id="ok_button">Ok</button>
</body>
</html>
The new lines are 16-27. Line 16 adds a <h2> element to create a heading for the rectangles setup. Lines 17 and 18 create a prompt for the input text box to get the number of rows. Lines 19 and 20 do the same thing for getting the number of columns. Line 21 is a prompt for getting the type of fixed color type. Lines 22-25 define a <select> element that allows the user to specify the type of fixed color type to use. Line 26 makes it so there is a blank line before the <button> that follows. Line 27 creates a <button id="ok_button"> that will be clicked on to draw the rectangles based on the user input.
Here is a partial screen shot that shows the input elements for the rectangles setup:
Next, we modify index.mjs so so that we can start to set up drawing the rows and columns of rectangles. This is just the first pass at doing this so we will only put in enough changes to test our code:
import { Rectangle } from "./svgshapes.mjs";
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
var svg_area;
function drawRectangles() {
console.log("drawRectangles called");
}
function init() {
console.log("init called");
svg_area = document.getElementById("svg_area");
const ok_button = document.getElementById("ok_button");
ok_button.addEventListener("click", drawRectangles);
}
The new lines are 9, 11-13 and 17-19. Line 9 adds the global variable, svg_area, that will hold the reference to the SVG container element. Lines 11-13 define the drawRectangles() function. All this function does right now is print 'drawRectangles called' to the Console. Line 17 gets the reference to the SVG container element and stores this reference as svg_area. Line 18 gets a reference to the Ok button. Line 19 makes it so that clicking on that Ok button will call the drawRectangles() function.
The following animated gif will demonstrate inside of CodeSandbox the testing of our changes to index.mjs:
Now that we know that clicking on the Ok button calls the correct function, let’s modify that function to read in the input values. Here is the next version of index.mjs:
import { Rectangle } from "./svgshapes.mjs";
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
var svg_area;
function drawRectangles() {
const rows_box = document.getElementById("rows_box");
const columns_box = document.getElementById("columns_box");
const fixed_type_select = document.getElementById("fixed_type_select");
const rows = Number(rows_box.value);
const columns = Number(columns_box.value);
const fixed_type = fixed_type_select.value;
console.log(`rows: ${rows}, cols: ${columns}`);
console.log(`fixed type: ${fixed_type}`);
}
function init() {
console.log("init called");
svg_area = document.getElementById("svg_area");
const ok_button = document.getElementById("ok_button");
ok_button.addEventListener("click", drawRectangles);
}
The new lines are 12-19. Lines 12-14 get references to the elements used to gather the rows, columns and fixed type, respectively. Lines 15 and 16 use the Number() function to convert the input from the text boxes into numbers and store in rows and columns, respectively. Line 17 gets the value from the <select> list and stores this in fixed_type. Line 18 uses a template. This is a string enclosed in backticks (` `). Within those backticks any place that has a ${some_variable}, will substitute in the value of that variable. This is called variable interpolation or substitution. So, line 18 will print the rows and columns entered by the user. Line 19 also uses variable interpolation and the backticks to print out the fixed_type string.
The following partial screen shot shows the result of the user entering values for the rows, columns and fixed type and then hitting the Ok button:
Setting up the nested for loops
Next, we will use the values for rows and columns to draw rectangles. For this pass, we will just draw "red" rectangles. Here is a diagram that shows how the x-coordinate and y-coordinate values have to change:
Each time we increase the column, the x-coordinate must increase by (width + 5), if 5 pixels is used for the horizontal gap. Each time we increase the row, the y-coordinate must increase by (height + 5), if 5 pixels is used for the vertical gap. The column values will go from 0 up to (columns - 1). The row values will go from 0 up to (rows - 1). So, generalized formulas for x and y would be:
x-coordinate = startX + column * (width + 5)
y-coordinate = startY + row * (height + 5)
To see this, you can substitute in values for the column and the row. So, for column = 0, we have:
x = startX
For column = 1, we have:
x = startX + (width + 5)
For column = 2, we have: x = startX + 2*(width + 5)
and so forth;
Here is a diagram that you may find helpful to visualize this:
In computer graphics, the way that x and y are defined is that x increases to the right, but y increases going down. It is important to keep this in mind, any time you need to think about positioning for computer graphics.
Using the formulas avoe, this will draw the rectangles in the rows and columns that the user specifies. Here is the new version of index.mjs:
import { Rectangle } from "./svgshapes.mjs";
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
var svg_area;
function drawRectangles() {
const rows_box = document.getElementById("rows_box");
const columns_box = document.getElementById("columns_box");
const fixed_type_select = document.getElementById("fixed_type_select");
const rows = Number(rows_box.value);
const columns = Number(columns_box.value);
const fixed_type = fixed_type_select.value;
const startX = 20;
const startY = 20;
const width = 35;
const height = 20;
let outstring = "";
for (let row = 0; row < rows; row++) {
for (let col = 0; col < columns; col++) {
const x = startX + col * (width + 5);
const y = startY + row * (height + 5);
outstring = outstring + `(${x},${y}) `;
}
outstring = outstring + "\n";
}
console.log(outstring);
}
function init() {
console.log("init called");
svg_area = document.getElementById("svg_area");
const ok_button = document.getElementById("ok_button");
ok_button.addEventListener("click", drawRectangles);
}
The new lines are 18-31. Line 18 sets the starting x-coordinate. Line 19 sets the starting y-coordinate. Line 20 defines the width to be 35 pixels. Line 21 defines the height to be 20 pixels. Hopefully, you can see how you could add input text boxes to allow the user to specify the width and the height of the rectangles. Lines 23-30 define the nested for loop. Note that both for loops are index-based for loops. This is the most straightforward way to calculate the (x,y) coordinates for the top left of each rectangle.
When you see nested for loops like this, think of the outer for loop as iterating over the rows, and the inner for loop iterating over the columns within each row. In other words, as you increase the column number, you are increasing x. When you increase the row number, you are increasing y. So, the outer for loop will set a value for the row, and this is fixed while the inner for loop iterates over the columns. So, with the nested for loops set up this way, you are drawing the first row with all its columns, then the second row and all its columns, etc.
On line 22, we create a variable called, outstring. Throughout the nested for loop, we will keep concatenating on to the end of outstring so that we can see how the (x,y) coordinates change as the loops run. We cannot simply use console.log() to print all along the way, as this will result in the output being broken up into more lines than we want. By updating outstring on line 27, we are adding the (x,y) coordinates for each column in a given row. Then, on line 29 we add a newline character, so that outstring will continue on the next line. This will simulate going to the next row. Note on lines 25 and 26, we use the formulas shown above to calculate the next x and y values. Finally, on line 31 we print out the entire contents of outstring all at once so we can see the output formatted into rows and columns.
Here is a screen shot showing the results of running the app now:
Outputting the rectangles
Now that the nested for loop has been set up, let’s actually draw some rectangles. To keep the changes minimal, we will make all the rectangles "red" for this pass. Here is the new version of index.mjs:
import { Rectangle } from "./svgshapes.mjs";
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
var svg_area;
function drawRectangles() {
const rows_box = document.getElementById("rows_box");
const columns_box = document.getElementById("columns_box");
const fixed_type_select = document.getElementById("fixed_type_select");
const rows = Number(rows_box.value);
const columns = Number(columns_box.value);
const fixed_type = fixed_type_select.value;
const startX = 20;
const startY = 20;
const width = 35;
const height = 20;
for (let row = 0; row < rows; row++) {
for (let col = 0; col < columns; col++) {
const x = startX + col * (width + 5);
const y = startY + row * (height + 5);
const rect1 = new Rectangle(x, y, width, height, "red");
rect1.id = `${row}-${col}`;
rect1.draw(svg_area);
}
}
}
function init() {
console.log("init called");
svg_area = document.getElementById("svg_area");
const ok_button = document.getElementById("ok_button");
ok_button.addEventListener("click", drawRectangles);
}
The new lines are 26-28. Line 26 creates a new Rectangle object at the calculated coordinates. The rectangles are all red at this point. That means that the fixed type value is not being used yet. Line 27 assigns an id to the Rectangle object. Note that this must be done before the draw() method is called on line 28.
The following screen shot shows the Elements tab of the Chrome browser’s DevTools displaying the id values. So, an id="0-0" means this is row 0, column 0. An id="2-3" means this is the rectangle at row 2, column 3. That would be the third physical row and fourth physical column (since the rows and columns are numbered starting at 0). Note that this is only visible if you display the page in a browser window. You can’t see this in the Preview of CodeSandbox.
Making use of the fixed type
Now that we can draw the rectangles using the nested for loops, let’s make use of the fixed type value to either draw the rectangles with Fixed row color or Fixed column color. So, we need to make use of a selection statement to handle these two cases.
Here is the new code for index.mjs that uses such a selection statement:
import { Rectangle } from "./svgshapes.mjs";
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
var svg_area;
function drawRectangles() {
const rows_box = document.getElementById("rows_box");
const columns_box = document.getElementById("columns_box");
const fixed_type_select = document.getElementById("fixed_type_select");
const rows = Number(rows_box.value);
const columns = Number(columns_box.value);
const fixed_type = fixed_type_select.value;
const startX = 20;
const startY = 20;
const width = 35;
const height = 20;
const colors = ["red", "blue", "green", "orange"];
for (let row = 0; row < rows; row++) {
for (let col = 0; col < columns; col++) {
let color = "red";
if (fixed_type === "Fixed row color") {
color = colors[row % 4];
} else if (fixed_type === "Fixed column color") {
color = colors[col % 4];
}
const x = startX + col * (width + 5);
const y = startY + row * (height + 5);
const rect1 = new Rectangle(x, y, width, height, color);
rect1.id = `${row}-${col}`;
rect1.draw(svg_area);
}
}
}
function init() {
console.log("init called");
svg_area = document.getElementById("svg_area");
const ok_button = document.getElementById("ok_button");
ok_button.addEventListener("click", drawRectangles);
}
The new lines are 22, 25, 26-30 and 33. Line 22 defines an array called colors that consists of four color strings. Line 25 sets a default value for the color variable. Lines 26-30 define the selection statement that will enforce the fixed_type value. So the first case, on lines 26-28, will be executed if fixed_type is Fixed row color. Line 27 will choose the color by setting it to colors[row%4]. This requires some explanation.
The % symbol is the modulus operator. The modulus operator returns the remainder of dividing the number before the modulus symbol by the number following the modulus symbol.
So, for example 6%4 returns 2, because 6 divided by 4 is 1 with a remainder of 2. The expression 3%4 returns a 3. This is because 3 divided by 4 is 0 with a remainder of 3. The modulus operator always returns the smallest positive number that needs to be added to a multiple of the second number to obtain the first number. So, 98%4 will return 2. This is because the closest multiple of 4 to 98 (without going over) is 96, and 96 + 2 = 98.
Since colors is an array, colors[0] is 'red', colors[1] is 'blue', colors[2] is 'green' and colors[3] is 'orange'. The reason why we use the % operators is to make sure that the index always ranges between 0 and 3. This means that if you have more than 4 rows or 4 columns, the colors will just repeat after 'orange' is reached. You can try working this out step by step.
Suppose you are setting Fixed row color, then:
when row = 0, row%4 = 0, so colors[row%4] is 'red'
when row = 1, row%4 = 1, so colors[row%4] is 'blue'
when row = 2, row%4 = 2, so colors[row%4] is 'green'
when row = 3, row%4 = 3, so colors[row%4] is 'orange'
when row = 4, row%4 = 0, so colors[row%4] is 'red'
when row = 5, row%4 = 1, so colors[row%4] is 'blue' and so on.
In setting the color this way, we only change the color if the row changes if we are using Fixed row color. Otherwise, if we are using Fixed column color, then the color=colors[col%4] only changes when the column changes. This produces the effect that we wanted.
The following animated gif (generated using the vite server page) shows the app in action:
If you want to replay the animation click on the Reload gif button.
Fixing a problem with the SVG container
One thing that we have forgotten to take into account is that each time that drawRectangles() is called, more SVG objects are appended to svg_area. This will make the app draw the rectangle incorrectly, and there will be more than one rectangle that has the same id. This is because we need to clear out svg_area each time we call drawRectangles(). Here is a new version of index.mjs that fixes this:
import { Rectangle } from "./svgshapes.mjs";
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
var svg_area;
function removeChildren(elem) {
while (elem.childNodes.length > 0) {
elem.removeChild(elem.childNodes[0]);
}
}
function drawRectangles() {
removeChildren(svg_area);
const rows_box = document.getElementById("rows_box");
const columns_box = document.getElementById("columns_box");
const fixed_type_select = document.getElementById("fixed_type_select");
const rows = Number(rows_box.value);
const columns = Number(columns_box.value);
const fixed_type = fixed_type_select.value;
const startX = 20;
const startY = 20;
const width = 35;
const height = 20;
const colors = ["red", "blue", "green", "orange"];
for (let row = 0; row < rows; row++) {
for (let col = 0; col < columns; col++) {
let color = "red";
if (fixed_type === "Fixed row color") {
color = colors[row % 4];
} else if (fixed_type === "Fixed column color") {
color = colors[col % 4];
}
const x = startX + col * (width + 5);
const y = startY + row * (height + 5);
const rect1 = new Rectangle(x, y, width, height, color);
rect1.id = `${row}-${col}`;
rect1.draw(svg_area);
}
}
}
function init() {
console.log("init called");
svg_area = document.getElementById("svg_area");
const ok_button = document.getElementById("ok_button");
ok_button.addEventListener("click", drawRectangles);
}
The new lines are 11-15 and 18. Lines 11-15 define the removeChildren() function. This uses a while loop that will keep removing the first child of the element, until all the children are removed. Line 18 calls this removeChildren() function inside of drawRectangles(), so that any time we click the Ok button, the svg_area is cleared out before adding the new SVG objects. You should run the application to verify that the rectangles are drawn correctly. An easy way to see this is to use 5 rows and 8 columns, and then redraw using 3 rows and 4 columns. Before applying this fix (to clear svg_area), this would not have drawn correctly.
Selecting a rectangle and changing its color
Since the rectangles are SVG objects with an id, we can select them. We need to be a little careful in how we do this because the SVG objects don’t become part of the document until we call the draw() method of the Rectangle class. Once draw() has been called, we can get a reference to the SVG object using document.getElementById(). Once we have that reference, we can apply an event listener to that object. Here is the new version of index.mjs that makes it so that we can change the color of the any rectangle we click on:
import { Rectangle } from "./svgshapes.mjs";
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
var svg_area;
function removeChildren(elem) {
while (elem.childNodes.length > 0) {
elem.removeChild(elem.childNodes[0]);
}
}
function drawRectangles() {
removeChildren(svg_area);
const rows_box = document.getElementById("rows_box");
const columns_box = document.getElementById("columns_box");
const fixed_type_select = document.getElementById("fixed_type_select");
const rows = Number(rows_box.value);
const columns = Number(columns_box.value);
const fixed_type = fixed_type_select.value;
const startX = 20;
const startY = 20;
const width = 35;
const height = 20;
const colors = ["red", "blue", "green", "orange"];
for (let row = 0; row < rows; row++) {
for (let col = 0; col < columns; col++) {
let color = "red";
if (fixed_type === "Fixed row color") {
color = colors[row % 4];
} else if (fixed_type === "Fixed column color") {
color = colors[col % 4];
}
const x = startX + col * (width + 5);
const y = startY + row * (height + 5);
const rect1 = new Rectangle(x, y, width, height, color);
rect1.id = `${row}-${col}`;
rect1.draw(svg_area);
const myrect = document.getElementById(`${row}-${col}`);
myrect.addEventListener("click", changeColor);
}
}
}
function changeColor(event) {
const rect2 = event.target;
rect2.setAttribute("fill", "yellow");
}
function init() {
console.log("init called");
svg_area = document.getElementById("svg_area");
const ok_button = document.getElementById("ok_button");
ok_button.addEventListener("click", drawRectangles);
}
The new lines are 43-44 and 49-52. Line 43 gets a reference to the SVG rectangle that was just added to the document. Line 44 makes it so that clicking on that object will call the changeColor() function. Lines 49-52 define the changeColor() function. Line 50 gets a reference to the event.target. That is the SVG object that was clicked on. Line 51, just sets the fill attribute to "yellow".
The following animated gif shows how clicking on any rectangle will change its color to "yellow":
Hit Reload gif to replay the animation.
As you can see, we can select any rectangle object we want, and cause a change to that object.
Summary
-
We created a JavaScript class called Rectangle, to help us with drawing a SVG rectangle.
-
For gathering input, we used input text boxes and a <select> element. The input text boxes were used to obtain the number of rows and number of columns for the rectangles we want to draw. The <select> element is used to let the user choose between having a fixed row color or a fixed column color.
-
We used both repetition statements and selection statements in our app. We used for loops, but we added some complexity by using nested for loops. The idea of processing rows and columns, with the outer for loop iterating over the rows, and the inner for loop iterating over the columns within a row was introduced. That is an important visualization for using nested for loops. A while loop was used to remove the SVG objects from the SVG container element. This is similar to what was done when drawing the HTML5 Canvas in the lesson: Graphics using HTML5 Canvas part 2: adding an unordered list.
-
A selection statement was used inside the nested for loops to choose the color scheme.
let color = "red";
if (fixed_type === "Fixed row color") {
color = colors[row % 4];
} else if (fixed_type === "Fixed column color") {
color = colors[col % 4];
}
This introduced the % (modulus) operator that is used for returning the remainder of dividing one number by another number. The first case makes it so that the colors change with the row changes, and the second case makes it so that the colors change with the column change.
-
The use of SVG objects made it so that our images can be assigned id values. This makes them selectable so they can be altered using JavaScript. The last part of the app, involved demonstrating this by clicking on a rectangle to change its color.