Categories
Quasar

Developing Vue Apps with the Quasar Library — Uploader, Video, and Virtual Scrolling

Quasar is a popular Vue UI library for developing good looking Vue apps.

In this article, we’ll take a look at how to create Vue apps with the Quasar UI library.

Customized Uploader Header

We can customize the header by populating the header slot:

<!DOCTYPE html>
<html>
  <head>
    <link
      href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons"
      rel="stylesheet"
      type="text/css"
    />
    <link
      href="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.min.css"
      rel="stylesheet"
      type="text/css"
    />
  </head>
  <body class="body--dark">
    <script src="https://cdn.jsdelivr.net/npm/vue@^2.0.0/dist/vue.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.umd.min.js"></script>
    <div id="q-app">
      <q-uploader url="http://localhost/upload" label="Custom header" multiple>
        <template v-slot:header="scope">
          <div class="row no-wrap items-center q-pa-sm q-gutter-xs">
            <q-btn
              v-if="scope.queuedFiles.length > 0"
              icon="clear_all"
              @click="scope.removeQueuedFiles"
              round
              dense
              flat
            >
              <q-tooltip>Clear All</q-tooltip>
            </q-btn>
            <q-btn
              v-if="scope.uploadedFiles.length > 0"
              icon="done_all"
              @click="scope.removeUploadedFiles"
              round
              dense
              flat
            >
              <q-tooltip>Remove Uploaded Files</q-tooltip>
            </q-btn>
            <q-spinner
              v-if="scope.isUploading"
              class="q-uploader__spinner"
            ></q-spinner>
            <div class="col">
              <div class="q-uploader__title">Upload your files</div>
              <div class="q-uploader__subtitle">
                {{ scope.uploadSizeLabel }} / {{ scope.uploadProgressLabel }}
              </div>
            </div>
            <q-btn
              v-if="scope.canAddFiles"
              type="a"
              icon="add_box"
              round
              dense
              flat
            >
              <q-uploader-add-trigger></q-uploader-add-trigger>
              <q-tooltip>Pick Files</q-tooltip>
            </q-btn>
            <q-btn
              v-if="scope.canUpload"
              icon="cloud_upload"
              @click="scope.upload"
              round
              dense
              flat
            >
              <q-tooltip>Upload Files</q-tooltip>
            </q-btn>

            <q-btn
              v-if="scope.isUploading"
              icon="clear"
              @click="scope.abort"
              round
              dense
              flat
            >
              <q-tooltip>Abort Upload</q-tooltip>
            </q-btn>
          </div>
        </template>
      </q-uploader>
    </div>
    <script>
      new Vue({
        el: "#q-app",
        data: {},
        methods: {}
      });
    </script>
  </body>
</html>

scope.queuedFiles has an array of files added for upload.

scope.removeQueuedFiles is a method to remove the queued files.

scope.uploadedFiles has an array of the uploaded files.

scope.removeQueuedFiles is a method to remove the uploaded files.

scope.canAddFiles is a boolean that indicates if we can add files.

scope.uploadSizeLabel has the total size of the files uploaded.

scope.uploadProgressLabel has the upload progress.

scope.canUpload lets us know if we can upload.

Video

Quasar comes with a video component to let us embed videos.

For example, we can add a YouTube video by writing:

<!DOCTYPE html>
<html>
  <head>
    <link
      href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons"
      rel="stylesheet"
      type="text/css"
    />
    <link
      href="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.min.css"
      rel="stylesheet"
      type="text/css"
    />
  </head>
  <body class="body--dark">
    <script src="https://cdn.jsdelivr.net/npm/vue@^2.0.0/dist/vue.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.umd.min.js"></script>
    <div id="q-app">
      <q-video src="https://www.youtube.com/embed/KfYrKGPUi94"> </q-video>
    </div>
    <script>
      new Vue({
        el: "#q-app",
        data: {},
        methods: {}
      });
    </script>
  </body>
</html>

We add it by setting src to the embed URL.

We can set the aspect ratio with the ratio prop:

<!DOCTYPE html>
<html>
  <head>
    <link
      href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons"
      rel="stylesheet"
      type="text/css"
    />
    <link
      href="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.min.css"
      rel="stylesheet"
      type="text/css"
    />
  </head>
  <body class="body--dark">
    <script src="https://cdn.jsdelivr.net/npm/vue@^2.0.0/dist/vue.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.umd.min.js"></script>
    <div id="q-app">
      <q-video :ratio="16/9" src="https://www.youtube.com/embed/KfYrKGPUi94">
      </q-video>
    </div>
    <script>
      new Vue({
        el: "#q-app",
        data: {},
        methods: {}
      });
    </script>
  </body>
</html>

Virtual Scrolling

Quasar comes with a virtual scrolling container component.

It lets us render data that are only shown on the screen instead of rendering everything, increasing performance.

For example, we write:

<!DOCTYPE html>
<html>
  <head>
    <link
      href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons"
      rel="stylesheet"
      type="text/css"
    />
    <link
      href="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.min.css"
      rel="stylesheet"
      type="text/css"
    />
  </head>
  <body class="body--dark">
    <script src="https://cdn.jsdelivr.net/npm/vue@^2.0.0/dist/vue.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.umd.min.js"></script>
    <div id="q-app">
      <q-virtual-scroll style="max-height: 300px;" :items="heavyList" separator>
        <template v-slot="{ item, index }">
          <q-item :key="index" dense>
            <q-item-section>
              <q-item-label>
                #{{ index }} - {{ item.label }}
              </q-item-label>
            </q-item-section>
          </q-item>
        </template>
      </q-virtual-scroll>
    </div>
    <script>
      const maxSize = 10000;
      const heavyList = [];

      for (let i = 0; i < maxSize; i++) {
        heavyList.push({
          label: `option ${i}`
        });
      }

      new Vue({
        el: "#q-app",
        data: {
          heavyList
        }
      });
    </script>
  </body>
</html>

to add it.

We add the item into the default slot to render it.

items has the items.

separator adds a separator between items.

Conclusion

We can add a file uploader into our Vue app with Quasar’s q-uploader component.

Also, we can add videos and virtual scrolling into our Vue app with Quasar.

Categories
Quasar

Developing Vue Apps with the Quasar Library — Uploader

Quasar is a popular Vue UI library for developing good looking Vue apps.

In this article, we’ll take a look at how to create Vue apps with the Quasar UI library.

Uploader

We can add an upload widget into our Vue app with Quasar’s q-uploader component.

For instance, we can write:

<!DOCTYPE html>
<html>
  <head>
    <link
      href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons"
      rel="stylesheet"
      type="text/css"
    />
    <link
      href="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.min.css"
      rel="stylesheet"
      type="text/css"
    />
  </head>
  <body class="body--dark">
    <script src="https://cdn.jsdelivr.net/npm/vue@^2.0.0/dist/vue.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.umd.min.js"></script>
    <div id="q-app">
      <q-uploader
        url="http://localhost/upload"
        label="Upload files"
        color="purple"
        square
        flat
        bordered
        style="max-width: 300px;"
      >
      </q-uploader>
    </div>
    <script>
      new Vue({
        el: "#q-app",
        data: {}
      });
    </script>
  </body>
</html>

We set the URL to upload to with the url prop.

label is displayed on the widget.

color changes the color of the top var.

bordered, square and flat change the appearance of the widget.

We can enable multiple uploads with the multiple prop.

And the batch prop lets us upload items in parallel.

Restrict File Type

We can restrict the file type allowed with the accept prop:

<!DOCTYPE html>
<html>
  <head>
    <link
      href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons"
      rel="stylesheet"
      type="text/css"
    />
    <link
      href="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.min.css"
      rel="stylesheet"
      type="text/css"
    />
  </head>
  <body class="body--dark">
    <script src="https://cdn.jsdelivr.net/npm/vue@^2.0.0/dist/vue.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.umd.min.js"></script>
    <div id="q-app">
      <q-uploader
        url="http://localhost/upload"
        label="Upload files"
        color="purple"
        square
        flat
        bordered
        style="max-width: 300px;"
        accept=".jpg, image/*"
        @rejected="onRejected"
      >
      </q-uploader>
    </div>
    <script>
      new Vue({
        el: "#q-app",
        data: {},
        methods: {
          onRejected(files) {
            console.log(files);
          }
        }
      });
    </script>
  </body>
</html>

We set the accept prop to the MIME types.

And the rejected event listener has the rejected files in an array in the first parameter of the listener function.

We can restrict the file size of the files that are accepted with the filter prop:

<!DOCTYPE html>
<html>
  <head>
    <link
      href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons"
      rel="stylesheet"
      type="text/css"
    />
    <link
      href="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.min.css"
      rel="stylesheet"
      type="text/css"
    />
  </head>
  <body class="body--dark">
    <script src="https://cdn.jsdelivr.net/npm/vue@^2.0.0/dist/vue.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.umd.min.js"></script>
    <div id="q-app">
      <q-uploader
        url="http://localhost/upload"
        label="Upload files"
        color="purple"
        square
        flat
        bordered
        style="max-width: 300px;"
        :filter="checkFileSize"
        @rejected="onRejected"
      >
      </q-uploader>
    </div>
    <script>
      new Vue({
        el: "#q-app",
        data: {},
        methods: {
          onRejected(files) {
            console.log(files);
          },
          checkFileSize(files) {
            return files.filter((file) => file.size < 1000);
          }
        }
      });
    </script>
  </body>
</html>

We pass the checkFileSize method to chekc the file size.

The size property is in bytes.

Async Factory Function

We can add an async factory function to create the q-uploader :

<!DOCTYPE html>
<html>
  <head>
    <link
      href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons"
      rel="stylesheet"
      type="text/css"
    />
    <link
      href="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.min.css"
      rel="stylesheet"
      type="text/css"
    />
  </head>
  <body class="body--dark">
    <script src="https://cdn.jsdelivr.net/npm/vue@^2.0.0/dist/vue.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.umd.min.js"></script>
    <div id="q-app">
      <q-uploader :factory="factoryFn" multiple> </q-uploader>
    </div>
    <script>
      new Vue({
        el: "#q-app",
        data: {},
        methods: {
          factoryFn(files) {
            return Promise.resolve({
              url: "http://localhost/upload"
            });
          }
        }
      });
    </script>
  </body>
</html>

We return a promise in the factoryFn method and pass the fucntion to the factory prop.

This will apply the settings in the resolved object.

Conclusion

We can add a file uploader into our Vue app with Quasar’s q-uploader component.

Categories
TypeScript

Introduction to TypeScript Enums

We look at how to define and use enums in TypeScript.

We look at how to define and use enums in TypeScript

If we want to define constants in JavaScript, we can use the const keyword. With TypeScript, we have another way to define a set of constants call the enums. Enums let us define a list of named constants. It’s handy for defining an entity that can take on a few possible values. TypeScript provides both numeric and string-based enums.

Numeric Enums

TypeScript has an enum type that’s not available in JavaScript. An enum type is a data type that has a set named values called elements, members, enumeral or enumerator of the type. They’re identifiers that act like constants in the language. In TypeScript, a numeric enum has a corresponding index associated with it. The members start with the index 0 by default, but it can be changed to start at any index we like and the subsequent members will have indexes that increment from that starting number instead. For example, we can write the following code to define a simple enum in TypeScript:

enum Fruit { Orange, Apple, Grape };

We can use enums by accessing the members like any other property. For example, in the Fruit enum, we can accept the members like in the following code:

console.log(Fruit.Orange);
console.log(Fruit.Apple);
console.log(Fruit.Grape);

Then console.log from the code above should get us 0 since we didn’t specify a starting index for the enum. We can specify the starting index of an enum with something like in the following code:

enum Fruit { Orange = 1, Apple, Grape };
console.log(Fruit.Orange);
console.log(Fruit.Apple);
console.log(Fruit.Grape);

Then we get the following logged from each console.log statement in order:

1
2
3

We can specify the same index for each member, but it wouldn’t be very useful:

enum Fruit { Orange = 1, Apple = 1, Grape };
console.log(Fruit.Orange);
console.log(Fruit.Apple);
console.log(Fruit.Grape);

Then we get:

1
1
2

from the console.log . As we can see, we specify the index pretty much however we want to change it. We can even have negative indexes:

enum Fruit { Orange = -1, Apple, Grape };
console.log(Fruit.Orange);
console.log(Fruit.Apple);
console.log(Fruit.Grape);

Then we get:

-1
0
1

from the console.log . To get an enum member by its index, we can just use the bracket notation like we access array entries by its index. For example, we can write the following code:

enum Fruit { Orange, Apple, Grape };
console.log(Fruit[0]);
console.log(Fruit[1]);
console.log(Fruit[2]);

Then we get:

Orange
Apple
Grape

Numeric enums can have computed values assigned to their members. For example, we can write a function to get a value for each enum member like in the following code:

const getValue = () => 2;

enum Fruit {
  Orange = getValue(),
  Apple = getValue(),
  Grape = getValue()
};

Note that we assigned a return value for each member. If we don’t do that for all of them like in the following code:

const getValue = () => 2;

enum Fruit {
  Orange = getValue(),
  Apple = getValue(),
  Grape
};

Then the TypeScript compiler won’t compile the code and will give an error “Enum member must have initializer.(1061)“. We can mix both constant and computed values in one enum, so we can write something like:

const getValue = () => 2;

enum Fruit {
  Orange = getValue(),
  Apple = 3,
  Grape = getValue()
};

String Enums

TypeScript enum members can also have string values. We can set the values of each member to a string by assigning strings to them like in the following code:

enum Fruit {
  Orange = 'Orange',
  Apple = 'Apple',
  Grape = 'Grape'
};

However, unlike numeric enums, we can’t assign computed values to them. For example, if we have the following:

const getValue = () => 'Orange';

enum Fruit {
  Orange = getValue(),
  Apple = 'Apple',
  Grape = 'Grape'
};

Then we would get the TypeScript compiler error message “Computed values are not permitted in an enum with string-valued members. (2553)” since computed values aren’t allowed for string-valued enums. String enums don’t have auto-incrementing behavior like numeric enums since they don’t have numerical values, but the values of the enum members are much clearer since each value is a meaningful value that’s clear to any human reading it.

In a single enum, we can have some members having numeric values and others having string values like in the following code:

enum Fruit {
  Orange = 2,
  Apple = 'Apple',
  Grape = 'Grape'
};

However, this is more confusing than having a single type of value for all enum members, so it’s not recommended to have mixed value types for different members for an enum.

Computed and Constant Members

Each enum member has a value associated with it which can either be constant or computed. An enum member is constant if the first member in the enum has no value explicitly assigned to it, which means that it’s assigned the value 0 by default. It can also be considered constant if it doesn’t have an explicit value assigned to it and the preceding enum member was a numeric constant, which means that it’ll have the value of the preceding member plus one. For example, if we have:

enum Fruit { Orange = 1, Apple, Grape };

Then Apple and Grape are both constant members since it’ll be automatically assigned the values 2 and 3 respectively. They’re also considered constant if each member has string values assigned to them. Also, if an enum references a previously defined enum member which can be from the same or a different enum. The return value of any operation assigned to constant enums like surrounding an enum expression with parentheses, doing unary arithmetic or bitwise operations to an enum expression like +, -, ~ , or doing binary arithmetic or bitwise operations like, -, *, /, %, <<, >>, >>>, &, |, ^ with enum expressions as operands are all considered constant enum expressions.

For example, the following enum is an enum with constant enum expressions:

enum Fruit {
  Orange = 1 + 2,
  Apple =  1 + 3,
  Grape = 1 + 4
};

The expressions are constant since they’re computed from any variable or return values of functions. Each member has values that’s computed from number values, rather than numbers assigned to variables or returned from functions.

The following is also an example of an enum with constant members:

enum Fruit {
  Orange = 1 + 2,
  Apple =  1 + 3,
  Grape = Orange + Apple
};

All the members including the last one are constant since the value of Grape is computed from Orange and Apple which are constant. Bitwise operations with both operands being constant values are also considered constants as we have in the following code:

enum Fruit {
  Orange = 1 | 2,
  Apple =  1 + 3,
  Grape = 'abc'.length
};

Anything else not described above is considered computed values. For example, if we have:

enum Fruit {
  Orange = 1 + 2,
  Apple =  1 + 3,
  Grape = 'abc'.length
};

Then Grape is a computed member since the expression we assigned to Grape is not computed from any constant member, and it involves getting a property from an object, which isn’t computed from a constant value.

If we want to define constants in JavaScript, we can use the const keyword. With TypeScript, we have another way to define a set of constants call the enums. Enums let us define a list of named constants. It’s handy for defining an entity that can take on a few possible values. TypeScript provides both numeric and string-based enums. TypeScript allows enum members to have numeric and string values. They can also be computed from values for other enum members or from any other expression we wish to assign. Constant enums are the ones that are computed from actual numerical values as operands or with actual values assigned to the member. All other values are computed member values.

Categories
JavaScript APIs

Create Custom JavaScript Error Objects

In JavaScript, there’s an Error class to let code throw exceptions. We can create custom error classes by extending the Error class with our own class and custom logic to throw more specific errors.

The Error class has the message , name , and stack files that we inherit from. Of course, like any other class, we can define our own fields and methods inside it.

Create a New Error Class

For example, to make data validation easier, we can make a ValidationError class which is thrown whenever our code encounters invalid data.

We can create a new class as follows:

class ValidationError extends Error {
  constructor(message, type) {
    super(message);
    this.name = "ValidationError";
    this.type = type;
  }
}

As we can see, we used the extends keyword to indicate that we want to inherit from the Error class.

Using Error Classes we Defined

After we defined our error classes, we can use our ValidationError class as follows:

try {
  let data = {
    email: 'abc'
  }
  if (!/[^@+@[^.+..+/.test(data.email)) {
    throw new ValidationError('invalid email', 'email error');
  }
} catch (ex) {
  console.log(ex);
}

In the console.log output, we should see ‘ValidationError: invalid email’ when we run the code.

We can also log the message , name , stack , and type properties individually:

try {
  let data = {
    email: 'abc'
  }
  if (!/[^@]+@[^.]+..+/.test(data.email)) {
    throw new ValidationError('invalid email', 'email error');
  }
} catch (ex) {
  const {
    message,
    name,
    stack,
    type
  } = ex;
  console.log(
    message,
    name,
    stack,
    type
  );
}

Then we should see:

  • invalid email logged for message
  • ValidationError logged for name
  • ValidationError: invalid email at window.onload for stack
  • email error logged for type

As with another type of object, we can use the instanceof operator to check if it’s an instance of ValidationError as follows:

try {
  let data = {
    email: 'abc'
  }
  if (!/[^@]+@[^.]+..+/.test(data.email)) {
    throw new ValidationError('invalid email', 'email error');
  }
} catch (ex) {
  if (ex instanceof ValidationError){
   console.log('validation error occurred');
  }
}

Then we should see ‘validation error occurred’ logged since we threw an instance of the ValidationError class.

We can also look at the name property to discern different kinds of errors. For example, we can write:

try {
  let data = {
    email: 'abc'
  }
  if (!/[^@]+@[^.]+..+/.test(data.email)) {
    throw new ValidationError('invalid email', 'email error');
  }
} catch (ex) {
  if (ex.name === 'ValidationError') {
    console.log('validation error occurred');
  }
}

Then we get the same thing as above.

Wrapping Exceptions

We can wrap lower-level exceptions, which are of instances of a subclass of a parent exception constructor and create higher-level errors.

To do this, we can catch and rethrow lower-level exceptions and catch them in higher-level code.

First, we have to create a parent and child classes which extend from the Error class as follows:

class FormError extends Error {
  constructor(message, type) {
    super(message);
    this.name = "FormError";
  }
}

class ValidationError extends FormError {
  constructor(message, type) {
    super(message);
    this.name = "ValidationError";
    this.type = type;
  }
}

In the code above, the FormError class extends the Error class and act as the parent class of the ValidationError class.

Then we write some code which makes use of our exception classes that we created:

const checkEmail = (data) => {
  try {
    if (!/[^@]+@[^.]+..+/.test(data.email)) {
      throw new ValidationError('invalid email', 'email error');
    }
  } catch (ex) {
    if (ex instanceof ValidationError) {
      throw new FormError(ex.name)
    } else {
      throw ex;
    }
  }
}

try {
  let data = {
    email: 'abc'
  }
  checkEmail(data);
} catch (ex) {
  if (ex instanceof FormError) {
    throw new FormError(ex.name)
  } else {
    throw ex;
  }
}

In the code above, we have a checkEmail function that throws an error if the data.email property has an invalid email. If a ValidationError is thrown, then we throw a new error.

Then we create a try...catch block after the checkEmail function to wrap around the function call and then we catch the exception in the catch block. We check for FormError instances and then throw a FormError if the caught exception is an instance of the FormError .

Otherwise, we rethrow the caught exception directly. We have this pattern so that we only deal with one kind of error at the top level, instead of checking for all kinds of error in top-level code.

The alternative is to have if...else statements to check for all kinds of errors at the top-level, which is more complex.

We can inherit from the Error class to create our own error classes. Then we can throw new error instances with the throw keyword.

In the class we define, we can add our own instance fields and methods.

When we catch the errors, we can use the insranceof operator to check for the type of error that’s thrown. We can also use the name property to do the same thing.

In our lower level code, we can catch errors of type of the sub-class error type and then rethrow an error of the instance of the parent class. Then we can catch the error of the higher-level type instead of checking for error instances of the lower-level type.

Categories
JavaScript APIs

Drawing Shapes on HTML Canvas — Arcs and Rectangles

The HTML canvas element lets us draw shapes and do various things with them. The most basic things include adding shapes, changing their color, and drawing paths.

In this article, we’ll look at how to draw shapes on the canvas, including arcs and rectangles

The Grid

Everything on the canvas is located on a grid. The items are positioned by coordinates. The top left corner is the origin (0, 0) and everything is placed relative to that. Normally 1 unit in the grid is 1 pixel on the screen.

The x-coordinate is larger as we move to the right and the y-coordinate increases as we go down.

Drawing Rectangles

Canvas supports only 2 primitive shapes — the rectangle and paths.

We can draw rectangles by using the fillRect method of the rendering context. It takes 4 arguments, which are the x and y coordinates of the top left corner respectively, and the width and height of the rectangle. The rectangle will be filled with a color.

The strokeRect method takes the same arguments as the fillRect method and let us draw a rectangular outline.

Finally, there’s the clearRect method, which clears the specified rectangular area, making it fully transparent. It takes the same arguments as the other 2 methods.

For example, we can use these methods as follows:

const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
ctx.fillRect(5, 5, 80, 80);
ctx.clearRect(15, 15, 60, 60);
ctx.strokeRect(50, 50, 50, 50);

given that we have the following HTML:

<canvas>

</canvas>

and CSS:

canvas {
  width: 200px;
  height: 200px;
  border: 1px solid black;
}

The fillRect draws a rectangle with the black background, which has a clear rectangle drawn with the clearRect method over it on the top left. The black rectangle has the top-left corner in (5, 5) and is 80×80 pixels large.

The clear rectangle has its top-left corner on (15, 15) and it’s 60×60 pixels large.

Then we have the rectangle drawn with the strokeRect method farther to the bottom right. Its top-left corner is at (50, 50) and it’s 50×50 pixels large.

In the end, we have the following result:

Drawing Paths

In addition to rectangles, we can draw paths, which are line segments that are connected together. It can be a different shape or color. The path can be closed.

We create paths with the following steps:

  1. We create the path
  2. Use drawing commands to draw into the path
  3. Then we can stroke or fill the path to render it.

There’re a few methods we need to call for drawing paths. We need to call the beginPath method to create a new path. Future drawing commands will be directed into the path and build it up.

Then we use 3 other methods to draw the path. We have the closePath method to add a straight line to the path, going to the start of the current subpath.

We use the stroke method to draw the shape by stroking its outline. Then we can use the fill method to fill the path’s content area if we wish.

We move to the coordinates of the screen with the moveTo method, which takes the x and y coordinates to move to as arguments. Then we can draw a line from that coordinate to another with the lineTo method, which takes the x and y coordinates to draw the line from wherever the current coordinate is.

For example, we can draw a triangle by writing:

const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
ctx.beginPath();
ctx.moveTo(100, 50);
ctx.lineTo(50, 100);
ctx.lineTo(150, 100);
ctx.fill();

We first start trying at (100, 50). Then we draw a line from (100, 50) to (50, 100) with the first lineTo call. Then we draw from (50, 100) to (150, 100) again with the lineTo method. Finally, we called fill to draw the shape by filling in the path’s content area.

In the end, we get:

moveTo Method and Arcs

The moveTo method is handy for moving the originating point of the drawing path without drawing anything.

It takes the x and y coordinates that we want to move to.

We can write:

const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
ctx.beginPath();
ctx.moveTo(125, 75);
ctx.arc(75, 75, 50, 0, 2 * Math.PI);
ctx.moveTo(135, 75);
ctx.arc(75, 75, 60, 0, 2 * Math.PI);
ctx.stroke();

To draw a circle, we move to the rightmost point of the circle with the moveTo method. Then we call the arc method to draw the arc. The first 2 arguments are the x and y coordinates of the center. The third argument is the radius, the 4th argument is the starting angle and the last argument if the ending angle.

In the code above, the starting angle is the 3 o’clock position. Clockwise or counterclockwise directions are relative to this point.

We can also pass in an additional boolean argument to specify if we want to draw counterclockwise.

In addition. There’s the arcTo method which takes the x and y coordinates of the starting point and the ending point respectively for the first 4 arguments. The 5th argument has the radius of the arc.

After writing that code, we get the following circles.

ctx.moveTo(125, 75);
ctx.arc(75, 75, 50, 0, 2 * Math.PI);

draws the smaller circle with a radius of 50 pixels, and:

ctx.moveTo(135, 75);
ctx.arc(75, 75, 60, 0, 2 * Math.PI);

draws the bigger circle with a radius of 60 pixels.

With the canvas element, we can draw shapes easily. There’re methods specifically for drawing rectangles. For curved shapes and lines, we can draw them with the arc and arcTo methods.

Shapes can be drawn filled or not depending on our preferences.

We can use the moveTo method to move to a given coordinate without drawing anything.