"Why nobody has already used this to do that?"
Hi everybody,
I bet I'm not the only one who has this type of thought. I've been in the web dev community for almost 12 years now, and I found myself thinking about this several times.
Disclaimer: my written English sucks, I'm trying to improve, please be gentle😇
Every time a new major web feature appears, like Canvas or WebAssembly, it's inevitable for me to start immagine all sort of new applications that are now possible. Then (almost) none of them come to life. Perhaps they are just less cool than how they seem to me, but I had nothing to lose and I developed one of them.
If I had to pick one web feature as my favourite, Canvas would win hands down.
IMHO it's the most underrated feature we have right now, and with this series of articles I want to share what I came up with until now, hoping more people will see canvas' potential as I do.
Because to me the real potential in Canvas isn't what we as developers can show to the users through it, but what users can give us back by using it. A Canvas on the web in the mobile era could really be the ultimate input for everything that's not a simple text, number or boolean.
My goal is to reach the most realistic drawing experience possible.
Spoiler:
Let's start from scratch. Canvas, we all know the basics:
// canvas creation
const myCanvas = document.createElement('canvas')
const ctx = myCanvas.getContext('2d')
myCanvas.width = 400
myCanvas.height = 150
container.appendChild(myCanvas)
// rect
ctx.fillStyle = 'rgb(255, 0, 0)'
ctx.fillRect(10, 10, 100, 80)
// circle
ctx.beginPath()
ctx.fillStyle = 'rgb(0, 0, 255)'
ctx.lineJoin = 'round'
ctx.lineCap = 'round'
ctx.arc(200, 50, 40, 0, 2 * Math.PI, true)
ctx.fill()
// image
ctx.drawImage(myImg, 280, 10, 80, 80)
We'll need these basics a lot, so why not encapsulate them (with a few more options):
const drawSquare = (destinationContext, x, y, alpha, size, color, rotation = 0) => {
const halfSize = size / 2
destinationContext.globalAlpha = alpha
destinationContext.fillStyle = color
if (rotation % 90) {
destinationContext.translate(x, y)
destinationContext.rotate(rotation)
destinationContext.fillRect(-halfSize, -halfSize, size, size)
destinationContext.rotate(-rotation)
destinationContext.translate(-x, -y)
} else {
destinationContext.fillRect(x - halfSize, y - halfSize, size, size)
}
}
const drawCircle = (destinationContext, x, y, alpha, size, color) => {
destinationContext.beginPath()
destinationContext.fillStyle = color
destinationContext.globalAlpha = alpha
destinationContext.lineJoin = 'round'
destinationContext.lineCap = 'round'
destinationContext.arc(x, y, size / 2, 0, 2 * Math.PI, true)
destinationContext.fill()
}
const drawImage = (destinationContext, x, y, alpha, size, image, rotation = 0) => {
const halfSize = size / 2
destinationContext.globalAlpha = alpha
if (rotation % 360) {
destinationContext.translate(x, y)
destinationContext.rotate(rotation)
destinationContext.drawImage(image, -halfSize, -halfSize, size, size)
destinationContext.rotate(-rotation)
destinationContext.translate(-x, -y)
} else {
destinationContext.drawImage(image, Math.round(x - halfSize), Math.round(y - halfSize), size, size)
}
}
And then use it:
drawSquare(ctx, 50, 150, 0.5, 80, 'rgb(255, 0, 0)', 30)
drawSquare(ctx, 110, 150, 0.7, 80, 'rgb(0, 255, 255)', -40)
drawCircle(ctx, 200, 150, 0.9, 50, 'rgb(255, 0, 0)')
drawCircle(ctx, 240, 150, 0.9, 60, 'rgb(255, 255, 0)')
drawCircle(ctx, 270, 150, 0.9, 70, 'rgb(0, 255, 255)')
drawImage(ctx, 350, 150, 0.6, 60, myImg, 45)
Note that I gave almost the same signature to all of them. This will be very useful later on.
So now we're ready to draw something on the screen on touch input (yeah I'm using touch input in this example, it would be almost the same with mouse down/move).
const defaultToolSize = 20
const currentToolColor = 'rgb(255, 0, 0)'
const handleTouch = (e) => {
const x = e.touches[0].clientX - myCanvas.offsetLeft
const y = e.touches[0].clientY - myCanvas.offsetTop
const alpha = e.touches[0].force || 1
drawCircle(ctx, x, y, alpha, defaultToolSize, currentToolColor)
}
myCanvas.addEventListener('touchstart', handleTouch)
myCanvas.addEventListener('touchmove', handleTouch)
Ok it's something. We can already see the skeleton of what will become ours line.
What if we would adapt the size based on the force pressure?
const defaultToolSize = 20
const sizeForceFactor = 2
const handleTouch = (e) => {
const x = e.touches[0].clientX - myCanvas.offsetLeft
const y = e.touches[0].clientY - myCanvas.offsetTop
const force = e.touches[0].force || 1
const size = defaultToolSize + (defaultToolSize * force)
drawCircle(ctx, x, y, force size, currentToolColor)
}
And why not to adapt the size based on the touch move speed?
const sizeSpeedFactor = 5
const speedFactorLengthUnit = 200
let lastTouch = {
x: -1,
y: -1,
force: 0,
}
// a bit of math
const round = (n, d = 0) => {
const m = d ? Math.pow(10, d) : 1
return Math.round(n * m) / m
}
const getDistanceBetweenTwoPoints = (x1, y1, x2, y2, decimals = 0) =>
round(Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)), decimals)
const handleTouch = (e) => {
const x = e.touches[0].clientX - myCanvas.offsetLeft
const y = e.touches[0].clientY - myCanvas.offsetTop
const force = e.touches[0].force || 1
const distance = lastTouch.x >= 0 ? getDistanceBetweenTwoPoints(lastTouch.x, lastTouch.y, x, y) : 0
const size = defaultToolSize +
(defaultToolSize * force) +
(defaultToolSize * sizeSpeedFactor * Math.min(distance / speedFactorLengthUnit, 1))
drawCircle(ctx, x, y, force, size, currentToolColor)
lastTouch = { x, y, force }
}
We could go further and use stylus inclination too to adapt size and alpha, but it's enough for now. I will treat those another time.
Starting from these simple point we will create a realistic line.
That's all for now, I really hope this topic could interest some of you. I spent a lot of time chasing the goal of reproducing a paper-like experience on the web, and I hope this could be useful somehow in the future.
Please leave a comment if you like the topic. I'd be happy to respond.
Thanks for your time! See you in the next post
Top comments (0)