In this part I finish my JavaScript Paint App. In this part I finish my JavaScript Paint App. It can draw brush strokes, lines, rectangles, circles, ellipses, and polygons. The app also saves and loads new images. When I cover other libraries and frameworks in the future like Node I will add additional capabilities.
All of the code and a transcript of the video follows below. Take notes as you watch to help you learn. I hope you have enjoyed the tutorial.
If you enjoy free tutorials like this consider donating $1 on Patreon. Starting next month, I will be uploading additional content every month only for the Patreons that help me keep my channel alive and free.
Code & Transcript
// JavaScript Paint App JavaScript Canvas API
// Reference to the canvas element
let canvas;
// Context provides functions used for drawing and
// working with Canvas
let ctx;
// Stores previously drawn image data to restore after
// new drawings are added
let savedImageData;
// Stores whether I'm currently dragging the mouse
let dragging = false;
let strokeColor = 'black';
let fillColor = 'black';
let line_Width = 2;
let polygonSides = 6;
// Tool currently using
let currentTool = 'brush';
let canvasWidth = 600;
let canvasHeight = 600;
// Stores whether I'm currently using brush
let usingBrush = false;
// Stores line x & ys used to make brush lines
let brushXPoints = new Array();
let brushYPoints = new Array();
// Stores whether mouse is down
let brushDownPos = new Array();
// Stores size data used to create rubber band shapes
// that will redraw as the user moves the mouse
class ShapeBoundingBox{
constructor(left, top, width, height) {
this.left = left;
this.top = top;
this.width = width;
this.height = height;
}
}
// Holds x & y position where clicked
class MouseDownPos{
constructor(x,y) {
this.x = x,
this.y = y;
}
}
// Holds x & y location of the mouse
class Location{
constructor(x,y) {
this.x = x,
this.y = y;
}
}
// Holds x & y polygon point values
class PolygonPoint{
constructor(x,y) {
this.x = x,
this.y = y;
}
}
// Stores top left x & y and size of rubber band box
let shapeBoundingBox = new ShapeBoundingBox(0,0,0,0);
// Holds x & y position where clicked
let mousedown = new MouseDownPos(0,0);
// Holds x & y location of the mouse
let loc = new Location(0,0);
// Call for our function to execute when page is loaded
document.addEventListener('DOMContentLoaded', setupCanvas);
function setupCanvas(){
// Get reference to canvas element
canvas = document.getElementById('my-canvas');
// Get methods for manipulating the canvas
ctx = canvas.getContext('2d');
ctx.strokeStyle = strokeColor;
ctx.lineWidth = line_Width;
// Execute ReactToMouseDown when the mouse is clicked
canvas.addEventListener("mousedown", ReactToMouseDown);
// Execute ReactToMouseMove when the mouse is clicked
canvas.addEventListener("mousemove", ReactToMouseMove);
// Execute ReactToMouseUp when the mouse is clicked
canvas.addEventListener("mouseup", ReactToMouseUp);
}
function ChangeTool(toolClicked){
document.getElementById("open").className = "";
document.getElementById("save").className = "";
document.getElementById("brush").className = "";
document.getElementById("line").className = "";
document.getElementById("rectangle").className = "";
document.getElementById("circle").className = "";
document.getElementById("ellipse").className = "";
document.getElementById("polygon").className = "";
// Highlight the last selected tool on toolbar
document.getElementById(toolClicked).className = "selected";
// Change current tool used for drawing
currentTool = toolClicked;
}
// Returns mouse x & y position based on canvas position in page
function GetMousePosition(x,y){
// Get canvas size and position in web page
let canvasSizeData = canvas.getBoundingClientRect();
return { x: (x - canvasSizeData.left) * (canvas.width / canvasSizeData.width),
y: (y - canvasSizeData.top) * (canvas.height / canvasSizeData.height)
};
}
function SaveCanvasImage(){
// Save image
savedImageData = ctx.getImageData(0,0,canvas.width,canvas.height);
}
function RedrawCanvasImage(){
// Restore image
ctx.putImageData(savedImageData,0,0);
}
function UpdateRubberbandSizeData(loc){
// Height & width are the difference between were clicked
// and current mouse position
shapeBoundingBox.width = Math.abs(loc.x - mousedown.x);
shapeBoundingBox.height = Math.abs(loc.y - mousedown.y);
// If mouse is below where mouse was clicked originally
if(loc.x > mousedown.x){
// Store mousedown because it is farthest left
shapeBoundingBox.left = mousedown.x;
} else {
// Store mouse location because it is most left
shapeBoundingBox.left = loc.x;
}
// If mouse location is below where clicked originally
if(loc.y > mousedown.y){
// Store mousedown because it is closer to the top
// of the canvas
shapeBoundingBox.top = mousedown.y;
} else {
// Otherwise store mouse position
shapeBoundingBox.top = loc.y;
}
}
// Returns the angle using x and y
// x = Adjacent Side
// y = Opposite Side
// Tan(Angle) = Opposite / Adjacent
// Angle = ArcTan(Opposite / Adjacent)
function getAngleUsingXAndY(mouselocX, mouselocY){
let adjacent = mousedown.x - mouselocX;
let opposite = mousedown.y - mouselocY;
return radiansToDegrees(Math.atan2(opposite, adjacent));
}
function radiansToDegrees(rad){
if(rad < 0){
// Correct the bottom error by adding the negative
// angle to 360 to get the correct result around
// the whole circle
return (360.0 + (rad * (180 / Math.PI))).toFixed(2);
} else {
return (rad * (180 / Math.PI)).toFixed(2);
}
}
// Converts degrees to radians
function degreesToRadians(degrees){
return degrees * (Math.PI / 180);
}
function getPolygonPoints(){
// Get angle in radians based on x & y of mouse location
let angle = degreesToRadians(getAngleUsingXAndY(loc.x, loc.y));
// X & Y for the X & Y point representing the radius is equal to
// the X & Y of the bounding rubberband box
let radiusX = shapeBoundingBox.width;
let radiusY = shapeBoundingBox.height;
// Stores all points in the polygon
let polygonPoints = [];
// Each point in the polygon is found by breaking the
// parts of the polygon into triangles
// Then I can use the known angle and adjacent side length
// to find the X = mouseLoc.x + radiusX * Sin(angle)
// You find the Y = mouseLoc.y + radiusY * Cos(angle)
for(let i = 0; i < polygonSides; i++){
polygonPoints.push(new PolygonPoint(loc.x + radiusX * Math.sin(angle),
loc.y - radiusY * Math.cos(angle)));
// 2 * PI equals 360 degrees
// Divide 360 into parts based on how many polygon
// sides you want
angle += 2 * Math.PI / polygonSides;
}
return polygonPoints;
}
// Get the polygon points and draw the polygon
function getPolygon(){
let polygonPoints = getPolygonPoints();
ctx.beginPath();
ctx.moveTo(polygonPoints[0].x, polygonPoints[0].y);
for(let i = 1; i < polygonSides; i++){
ctx.lineTo(polygonPoints[i].x, polygonPoints[i].y);
}
ctx.closePath();
}
// Called to draw the line
function drawRubberbandShape(loc){
ctx.strokeStyle = strokeColor;
ctx.fillStyle = fillColor;
if(currentTool === "brush"){
// Create paint brush
DrawBrush();
} else if(currentTool === "line"){
// Draw Line
ctx.beginPath();
ctx.moveTo(mousedown.x, mousedown.y);
ctx.lineTo(loc.x, loc.y);
ctx.stroke();
} else if(currentTool === "rectangle"){
// Creates rectangles
ctx.strokeRect(shapeBoundingBox.left, shapeBoundingBox.top, shapeBoundingBox.width, shapeBoundingBox.height);
} else if(currentTool === "circle"){
// Create circles
let radius = shapeBoundingBox.width;
ctx.beginPath();
ctx.arc(mousedown.x, mousedown.y, radius, 0, Math.PI * 2);
ctx.stroke();
} else if(currentTool === "ellipse"){
// Create ellipses
// ctx.ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle)
let radiusX = shapeBoundingBox.width / 2;
let radiusY = shapeBoundingBox.height / 2;
ctx.beginPath();
ctx.ellipse(mousedown.x, mousedown.y, radiusX, radiusY, Math.PI / 4, 0, Math.PI * 2);
ctx.stroke();
} else if(currentTool === "polygon"){
// Create polygons
getPolygon();
ctx.stroke();
}
}
function UpdateRubberbandOnMove(loc){
// Stores changing height, width, x & y position of most
// top left point being either the click or mouse location
UpdateRubberbandSizeData(loc);
// Redraw the shape
drawRubberbandShape(loc);
}
// Store each point as the mouse moves and whether the mouse
// button is currently being dragged
function AddBrushPoint(x, y, mouseDown){
brushXPoints.push(x);
brushYPoints.push(y);
// Store true that mouse is down
brushDownPos.push(mouseDown);
}
// Cycle through all brush points and connect them with lines
function DrawBrush(){
for(let i = 1; i < brushXPoints.length; i++){
ctx.beginPath();
// Check if the mouse button was down at this point
// and if so continue drawing
if(brushDownPos[i]){
ctx.moveTo(brushXPoints[i-1], brushYPoints[i-1]);
} else {
ctx.moveTo(brushXPoints[i]-1, brushYPoints[i]);
}
ctx.lineTo(brushXPoints[i], brushYPoints[i]);
ctx.closePath();
ctx.stroke();
}
}
function ReactToMouseDown(e){
// Change the mouse pointer to a crosshair
canvas.style.cursor = "crosshair";
// Store location
loc = GetMousePosition(e.clientX, e.clientY);
// Save the current canvas image
SaveCanvasImage();
// Store mouse position when clicked
mousedown.x = loc.x;
mousedown.y = loc.y;
// Store that yes the mouse is being held down
dragging = true;
// Brush will store points in an array
if(currentTool === 'brush'){
usingBrush = true;
AddBrushPoint(loc.x, loc.y);
}
};
function ReactToMouseMove(e){
canvas.style.cursor = "crosshair";
loc = GetMousePosition(e.clientX, e.clientY);
// If using brush tool and dragging store each point
if(currentTool === 'brush' && dragging && usingBrush){
// Throw away brush drawings that occur outside of the canvas
if(loc.x > 0 && loc.x < canvasWidth && loc.y > 0 && loc.y < canvasHeight){
AddBrushPoint(loc.x, loc.y, true);
}
RedrawCanvasImage();
DrawBrush();
} else {
if(dragging){
RedrawCanvasImage();
UpdateRubberbandOnMove(loc);
}
}
};
function ReactToMouseUp(e){
canvas.style.cursor = "default";
loc = GetMousePosition(e.clientX, e.clientY);
RedrawCanvasImage();
UpdateRubberbandOnMove(loc);
dragging = false;
usingBrush = false;
}
// Saves the image in your default download directory
function SaveImage(){
// Get a reference to the link element
var imageFile = document.getElementById("img-file");
// Set that you want to download the image when link is clicked
imageFile.setAttribute('download', 'image.png');
// Reference the image in canvas for download
imageFile.setAttribute('href', canvas.toDataURL());
}
function OpenImage(){
let img = new Image();
// Once the image is loaded clear the canvas and draw it
img.onload = function(){
ctx.clearRect(0,0,canvas.width, canvas.height);
ctx.drawImage(img,0,0);
}
img.src = 'image.png';
}
Jstut7.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width = device-width, initial-scale = 1">
<title>JavaScript Paint App</title>
<link rel="stylesheet" type="text/css" href="mainstyle.css">
<script src="jscolor.js"></script>
<script src="jstut7-2.js"></script>
</head>
<body>
<div class="wrapper">
<div class="toolbar">
<a class="selected" href="#" id="open" onclick="OpenImage()"><img src="open-icon.png"></a>
<a href="#" id="save" onclick="SaveImage()"><img src="save-icon.png"></a>
<a href="#" id="brush" onclick="ChangeTool('brush')"><img src="brush-icon.png"></a>
<a href="#" id="line" onclick="ChangeTool('line')"><img src="line-icon.png"></a>
<a href="#" id="rectangle" onclick="ChangeTool('rectangle')"><img src="rectangle-icon.png"></a>
<a href="#" id="circle" onclick="ChangeTool('circle')"><img src="circle-icon.png"></a>
<a href="#" id="ellipse" onclick="ChangeTool('ellipse')"><img src="ellipse-icon.png"></a>
<a href="#" id="polygon" onclick="ChangeTool('polygon')"><img src="polygon-icon.png"></a>
</div><br>
<canvas id="my-canvas" width="600" height="600"></canvas>
<div id="img-data-div">
<a href="#" id="img-file" download="image.png">download image</a>
</div>
</div>
</body>
</html>
mainstyle.css
.wrapper {
max-width: 900px;
margin: auto;
font-family: "Arial";
}
.toolbar{
width: 100%;
background-color: #444444;
overflow: auto;
}
.toolbar a {
float: left;
width: 11%;
text-align: center;
padding: 6px 5px;
transition: all 0.5s ease;
color: white;
}
/* Change color on hover */
.toolbar a:hover {
background-color: #000;
}
/* Change color on selected icon */
.selected {
background-color: #000;
}
#my-canvas{
width: 100%;
border: 3px solid #000000;
}
#img-data-div{
width: 100%;
max-width: 900px;
height: 200px;
}
/* Resize image to container */
.toolbar a img{
max-width:100%;
height:auto;
}