2
\$\begingroup\$

So for a class project, I have to figure out the most efficient way to draw on a canvas.

First thing that came to mind was using moveTo and lineTo repeatedly following mouse cursor, and thats exactly what I did.

I also added a strange "liner" pen which draws lines outwards from a point (side effect of bug, which I then turned into a tool)

Furthermore I added a coloring and sizing option, so you can be creative with it.

Sizing option, Spray tool, and Color Options are all non-functional rn.

I'm not really sure how to get the color or sizing option working if anyone wants to take a crack at it.

I want to know if anyone sees any bad practice uses that I have created, or has any improvements/tips that I should keep in mind as I continue development on the canvas.

Also the GUI and Buttons and everything, its all temporary, my main job is to actually finish the project.

https://repl.it/@skylerspark/Dynamic-Drawing-Utility-DDU

Heres the live development version, and heres the current version as of posting this code review:

const query = document.querySelector.bind(document);

// Config
let curLine = [0, 0],
  curColor = "#000000",
  curSize = 2;

// Cursor Coords
function drawingBoardGetMouse(cvs, e) {
  let rect = cvs.getBoundingClientRect(),
    scaleX = cvs.width / rect.width,
    scaleY = cvs.height / rect.height;
  return {
    x: (e.clientX - rect.left) * scaleX,
    y: (e.clientY - rect.top) * scaleY
  };
}

// Dynamic Drawing Utility (DDU)
class drawingBoard {
  constructor(canvas) {
    // Generate a dynamic canvas selector, or manually select it yourself via the main args
    this.cvs = canvas || document.querySelector("canvas");
    this.ctx = this.cvs.getContext("2d");

    this.cvs.addEventListener("mousedown", e => {
      let m = drawingBoardGetMouse(this.cvs, e);
      curLine = [m.x, m.y];
      this.mouseDown = true;
    });

    this.cvs.addEventListener("mouseup", e => {
      this.mouseDown = false;
    });

    this.cvs.addEventListener("mousemove", this.onMouseMove.bind(this));
  }
  draw(e) { // Free Draw Pen
    let m = drawingBoardGetMouse(this.cvs, e);
    this.ctx.beginPath();
    this.ctx.fillStyle = curColor;
    this.ctx.lineWidth = curSize;
    this.ctx.moveTo(curLine[0], curLine[1]);
    this.ctx.lineTo(m.x, m.y);
    this.ctx.stroke();
    curLine = [m.x, m.y];
  }
  draw2(e) { // Line Draw Pen
    let m = drawingBoardGetMouse(this.cvs, e);
    this.ctx.beginPath();
    this.ctx.fillStyle = curColor;
    this.ctx.lineWidth = curSize;
    this.ctx.moveTo(curLine[0], curLine[1]);
    this.ctx.lineTo(m.x, m.y);
    this.ctx.stroke();
  }
  onMouseMove(event) { // Detect movement
    if (!this.mouseDown) {
      return; // If mouse isnt held down, dont draw
    }
    if (this.drawingMode == 1) {
      return this.draw(event); // Free-draw
    } else if (this.drawingMode == 2) {
      return this.draw2(event); // Line-draw
    } else if (this.drawingMode == 3) {
      return this.draw3(event); // Spray-draw
    }
  }
  dragAndDraw() {
    this.drawingMode = 1;
  }
  dragAndLine() {
    this.drawingMode = 2;
  }
  dragAndSpray() {
    this.drawingMode = 3;
  }
}

// API Usage

const draw = new drawingBoard(); // Initialize new board

query(".color").addEventListener("input", e => {
  curColor = e.target.value;
});

query(".drawer").addEventListener("click", () => {
  draw.dragAndDraw();
});

query(".liner").addEventListener("click", () => {
  draw.dragAndLine();
});

query(".sprayer").addEventListener("click", () => {
  draw.dragAndSpray();
});
* {
  text-align: center;
  vertical-align: top;
  box-sizing: border-box;
}

.canvas {
  display: inline-block;
  border: 1px solid black;
}

.buttons {
  display: inline-block;
}

button,
input {
  width: 8em;
  margin-bottom: 2px;
  margin-left: -2px;
}

input {
  text-align: left;
}
<canvas class="canvas" width="300" height="300"></canvas>
<div class="buttons">
  <input type="color" class="color"><br>
  <input type="text" class="size" placeholder="Size"><br>
  <button class="drawer">Free Draw</button><br>
  <button class="liner">Line Draw</button><br>
  <button class="sprayer">Spray Draw</button>
</div>

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

First, draw and draw2 looks pretty similar to me, you can DRY them

class drawingBoard {

    // ...

    draw (e) {
        this.draw2(e)
        let m = drawingBoardGetMouse(this.cvs, e)
        curLine = [m.x, m.y]
    }
    draw2 (e) {
        let m = drawingBoardGetMouse(this.cvs, e)
        this.ctx.beginPath()
        this.ctx.fillStyle = curColor
        this.ctx.lineWidth = curSize
        this.ctx.moveTo(curLine[0], curLine[1])
        this.ctx.lineTo(m.x, m.y)
        this.ctx.stroke()
    }

    // ...

}

Then, there is a "not so useful" if statement

if (this.drawingMode == 1) {
    return this.draw(event) // Free-draw
} else if (this.drawingMode == 2) {
    return this.draw2(event) // Line-draw
} else if (this.drawingMode == 3) {
    return this.draw3(event) // Spray-draw
}

Here is how you can replace it, which will improve the readability and elegance of the code

// ...

class drawingBoard {

    // ...

    onMouseMove (event) {
        if (!this.mouseDown) {
            return // If mouse isnt held down, dont draw
        }
        if (this.drawingMode in this) {
            this[this.drawingMode](event)
        }
    }

    dragAndDraw (e) {
        this.dragAndLine(e)
        let m = drawingBoardGetMouse(this.cvs, e)
        curLine = [m.x, m.y]
    }

    dragAndLine (e) {
        let m = drawingBoardGetMouse(this.cvs, e)
        this.ctx.beginPath()
        this.ctx.fillStyle = curColor
        this.ctx.lineWidth = curSize
        this.ctx.moveTo(curLine[0], curLine[1])
        this.ctx.lineTo(m.x, m.y)
        this.ctx.stroke()
    }

    dragAndSpray () {
        // TODO ?
    }
}


// ...


query('.drawer').addEventListener('click', () => {
    draw.drawingMode = 'dragAndDraw'
})

query('.liner').addEventListener('click', () => {
    draw.drawingMode = 'dragAndLine'
})

query('.sprayer').addEventListener('click', () => {
    draw.drawingMode = 'dragAndSpray'
})

You can move the global variables into instance variables so you can create multiple instances of drawingBoard if you want

class drawingBoard {

    constructor (canvas) {

        // ...

        this.curLine = [0, 0]
        this.curColor = '#000000'
        this.curSize = 2


        this.cvs.addEventListener('mousedown', e => {
            let m = drawingBoardGetMouse(this.cvs, e)
            this.curLine = [m.x, m.y]
            this.mouseDown = true
        })

        // ...
        
    }

    dragAndDraw (e) {
        this.dragAndLine(e)
        this.curLine = [m.x, m.y]
    }
    
    dragAndLine (e) {
        // ...

        this.ctx.fillStyle = this.curColor
        this.ctx.lineWidth = this.curSize
        this.ctx.moveTo(this.curLine[0], this.curLine[1])

        // ...
    }
    
}


Here is my final take with some flavor adjustments

class DrawingBoard {

    constructor (canvas) {

        this.canvas  = canvas || document.querySelector('canvas')
        this.ctx     = this.canvas.getContext('2d')

        this.currentLine = {x: 0, y: 0}
        this.color       = '#000000'
        this.size        = 2

        registerMouseEvents(this)
    }

    draw (mouseEvent) {
        if (!this.mouseDown) {
            return
        }

        if (this.drawingMode in this) {
            this[this.drawingMode](mouseEvent)
        }
    }

    dragAndDraw (e) {
        this.dragAndLine(e)
        this.currentLine = getMouseCoords(this.canvas, e)
    }

    dragAndLine (e) {
        let coords = getMouseCoords(this.canvas, e)
        this.ctx.beginPath()
        this.ctx.fillStyle = this.color
        this.ctx.lineWidth = this.size
        this.ctx.moveTo(this.currentLine.x, this.currentLine.y)
        this.ctx.lineTo(coords.x, coords.y)
        this.ctx.stroke()
    }

    dragAndSpray () {
        // TODO ?
    }
}



function registerMouseEvents (drawingBoard) {

    const canvas = drawingBoard.canvas

    canvas.addEventListener('mousedown', e => {
        drawingBoard.currentLine = getMouseCoords(canvas, e)
        drawingBoard.mouseDown = true
    })

    canvas.addEventListener('mouseup', e => {
        drawingBoard.mouseDown = false
    })

    canvas.addEventListener('mousemove', e => {
        drawingBoard.draw(e)
    })

}



function getMouseCoords (canvas, e) {

    let rect   = canvas.getBoundingClientRect()
    let scaleX = canvas.width  / rect.width
    let scaleY = canvas.height / rect.height

    return {
        x: (e.clientX - rect.left) * scaleX,
        y: (e.clientY - rect.top)  * scaleY
    }

}



// USAGE

const query = document.querySelector.bind(document)
const drawingBoard = new DrawingBoard()


query('.color').addEventListener('input', e => {
    drawingBoard.color = e.target.value
})

query('.drawer').addEventListener('click', () => {
    drawingBoard.drawingMode = 'dragAndDraw'
})

query('.liner').addEventListener('click', () => {
    drawingBoard.drawingMode = 'dragAndLine'
})

query('.sprayer').addEventListener('click', () => {
    drawingBoard.drawingMode = 'dragAndSpray'
})
* {
  text-align: center;
  vertical-align: top;
  box-sizing: border-box;
}

.canvas {
  display: inline-block;
  border: 1px solid black;
}

.buttons {
  display: inline-block;
}

button,
input {
  width: 8em;
  margin-bottom: 2px;
  margin-left: -2px;
}

input {
  text-align: left;
}
<canvas class="canvas" width="300" height="300"></canvas>
<div class="buttons">
  <input type="color" class="color"><br>
  <input type="text" class="size" placeholder="Size"><br>
  <button class="drawer">Free Draw</button><br>
  <button class="liner">Line Draw</button><br>
  <button class="sprayer">Spray Draw</button>
</div>

\$\endgroup\$
1
  • \$\begingroup\$ Thankyou! This is very informing, I didnt realise some of these issues up front, thanks a ton! \$\endgroup\$ Commented Mar 16, 2020 at 0:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.