I am making a drawing feature on a canvas, but am stuck on the zoom part. Can anyone help me? When I am in a maximum zoomed out state, I am able to draw exactly where the mouse cursor is. However, when I zoom in, the lines I draw have an offset from where the mouse is. The formulae I am using for getting the arguments to my drawLines function's adjustedX and adjustedY values are:
const adjustedX = e.offsetX * viewportTransform.scale - viewportTransform.x;
const adjustedY = e.offsetY * viewportTransform.scale -viewportTransform.y;
Can someone help me fix this? I have attached all the code in the html file. It is HTML and vanilla JS.
function showLines() {
console.log(lines)
}
function toggleDrawMode() {
isDrawingMode = !isDrawingMode
}
const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
let lines = [];
const viewportTransform = {
x: 0,
y: 0,
scale: 1
}
// From here on, everything we'll write will go below 👇
const drawRect = (x, y, width, height, color) => {
ctx.fillStyle = color
ctx.fillRect(x, y, width, height)
}
let isDrawingMode = true
const penWidth = 5
const penColor = 'red'
const drawPixels = (x, y) => {
ctx.beginPath()
ctx.arc(x, y, penWidth, 2 * Math.PI, false)
ctx.fillStyle = penColor
ctx.fill()
}
const erasePixels = (x, y) => {
ctx.save()
ctx.beginPath()
ctx.arc(x, y, penWidth, 2 * Math.PI, false)
ctx.globalCompositeOperation = 'destination-out'
ctx.fillStyle = 'white'
ctx.fill()
ctx.restore()
}
const render = () => {
// New code 👇
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.setTransform(viewportTransform.scale, 0, 0, viewportTransform.scale, viewportTransform.x, viewportTransform.y);
// New Code 👆
// drawRect(0, 0, 100, 100, 'red');
// drawRect(200, 200, 100, 100, 'blue');
// drawPixels(400, 400)
// Ok, so we DO need to redraw the lines. That means we DO need to KEEP TRACK of them!
// This drawing just needs absolute figures, the same as they were when the line was being made! The viewportTransform will automatically adjust shit here and there.
lines.forEach((line, idx) => {
// console.log(`letsdraw this line #${idx}`, line, `from the start pos (${previousXDrawing}, ${previousYDrawing})`);
// drawPixels(line.x, line.y)
if (line.isEraseLine) {
eraseLine(line.x, line.y, line.previousX, line.previousY)
} else {
drawLine(line.x, line.y, line.previousX, line.previousY)
}
// TODO: bullshit most likely
// previousXDrawing = line.x
// previousYDrawing = line.y
});
}
render()
// drawPixels(400, 400)
// lines.push({ x: 400, y: 400 })
// We need to keep track of our previous mouse position for later
let previousX = 0,
previousY = 0;
let previousXDrawing = 0,
previousYDrawing = 0;
const updatePanning = (e) => {
const localX = e.clientX;
const localY = e.clientY;
viewportTransform.x += localX - previousX;
viewportTransform.y += localY - previousY;
previousX = localX;
previousY = localY;
}
// If we are trying to zoom out to a level lesser than zoom level 1, then we will not do anything
const isZoomAllowed = (viewportTransform, deltaY) => {
return viewportTransform.scale + deltaY * -0.01 >= 1
}
const updateZooming = (e) => {
const oldScale = viewportTransform.scale;
const oldX = viewportTransform.x;
const oldY = viewportTransform.y;
const localX = e.clientX;
const localY = e.clientY;
const previousScale = viewportTransform.scale;
const newScale = viewportTransform.scale += e.deltaY * -0.01;
const newX = localX - (localX - oldX) * (newScale / previousScale);
const newY = localY - (localY - oldY) * (newScale / previousScale);
viewportTransform.x = newX;
viewportTransform.y = newY;
viewportTransform.scale = newScale;
}
const drawLine = (x, y, previousPosX, previousPosY) => {
ctx.beginPath()
ctx.moveTo(previousPosX, previousPosY)
ctx.lineTo(x, y)
ctx.strokeStyle = penColor
ctx.lineWidth = penWidth
ctx.lineCap = 'round'
ctx.stroke()
}
const eraseLine = (x, y, previousPosX, previousPosY) => {
ctx.save()
ctx.beginPath()
ctx.moveTo(previousPosX, previousPosY)
ctx.lineTo(x, y)
ctx.globalCompositeOperation = 'destination-out'
// ctx.strokeStyle = 'white' // NOT REALLY NEEDED. Destination out means you're just removing the effects of the pen, regardless of whatever colors they were.
ctx.lineWidth = penWidth
ctx.lineCap = 'round'
ctx.stroke()
ctx.restore()
}
const onMouseMove = (e) => {
// Handle the case of drawing here. If drawing mode is on, then that should be handled.
if (isDrawingMode) {
// FINAL: yes this is the final solution! I have got it! Eureka!!!! yippieee
// TODO: change this PART B
const adjustedX = e.offsetX * viewportTransform.scale - viewportTransform.x;
const adjustedY = e.offsetY * viewportTransform.scale - viewportTransform.y;
// const adjustedX = (e.clientX + viewportTransform.x) * viewportTransform.scale ;
// const adjustedY = (e.clientY + viewportTransform.y) * viewportTransform.scale ;
console.log('X walay: ', e.offsetX, viewportTransform.scale, viewportTransform.x);
console.log('Y walay: ', e.offsetY, viewportTransform.scale, viewportTransform.y);
var bounding = canvas.getBoundingClientRect();
var x = e.clientX - bounding.left;
var y = e.clientY - bounding.top;
const xToDraw = adjustedX
const yToDraw = adjustedY
if (e.shiftKey) {
// erasePixels(xToDraw, yToDraw)
eraseLine(xToDraw, yToDraw, previousXDrawing, previousYDrawing)
lines.push({
x: xToDraw,
y: yToDraw,
previousX: previousXDrawing,
previousY: previousYDrawing,
isEraseLine: true
}) // TODO: of course we somehow need to remember that this is a erased line. and handle it that way
} else {
// drawPixels(xToDraw, yToDraw)
// drawPixels(400, 400)
drawLine(xToDraw, yToDraw, previousXDrawing, previousYDrawing)
lines.push({
x: xToDraw,
y: yToDraw,
previousX: previousXDrawing,
previousY: previousYDrawing,
isEraseLine: false
})
}
// bogus attempt TODO: problem either here!
previousXDrawing = adjustedX
previousYDrawing = adjustedY
console.log('on moving of mouse, these were the assigned prev x and y: ', previousXDrawing, previousYDrawing);
} else {
updatePanning(e)
render()
}
// updatePanning(e)
// render()
// console.log(e)
}
const onMouseWheel = (e) => {
if (isZoomAllowed(viewportTransform, e.deltaY)) {
updateZooming(e)
render()
}
// console.log(e)
}
canvas.addEventListener("wheel", onMouseWheel);
canvas.addEventListener("mousedown", (e) => {
// This is needed for ensuring smooth panning
previousX = e.clientX;
previousY = e.clientY;
// This is my attempt to make the drawLine logic work TODO: or problem is here
if (isDrawingMode) {
// TODO: change this PARTA
const adjustedX = e.offsetX * viewportTransform.scale - viewportTransform.x;
const adjustedY = e.offsetY * viewportTransform.scale - viewportTransform.y;
// const adjustedX = (e.clientX + viewportTransform.x) * viewportTransform.scale;
// const adjustedY = (e.clientY + viewportTransform.y) * viewportTransform.scale ;
var bounding = canvas.getBoundingClientRect();
var x = e.clientX - bounding.left;
var y = e.clientY - bounding.top;
previousXDrawing = adjustedX
previousYDrawing = adjustedY
// previousXDrawing = e.offsetX
// previousYDrawing = e.offsetY
// TODO: for some reason, over here e.offsetX is a lot.
// maybe we are using the wrong key for comparing x and y coordinates for drawing, and need something else
console.log('in adding event listener, these were prev X and Y: ', previousXDrawing, previousYDrawing)
}
canvas.addEventListener("mousemove", onMouseMove);
})
canvas.addEventListener("mouseup", (e) => {
canvas.removeEventListener("mousemove", onMouseMove);
})
<canvas width="500" height="500" id="canvas" style="border: 2px solid black; background-color: yellow;"></canvas>
<label for="">Drawing Mode</label>
<input type="checkbox" checked onchange="toggleDrawMode()">
<button onclick="showLines()">Show stats of lines</button>
For video demo of issue, see the below video: https://www.facebook.com/abdullahahmadaak/videos/560288413454083/?idorvanity=1094897594327615¬if_id=1733394446600296¬if_t=video_processed&ref=notif
The core of the solution is:
// What you already had: resolve x,y in pixels relative to the canvas element
const bounding = canvas.getBoundingClientRect();
const x = e.clientX - bounding.left;
const y = e.clientY - bounding.top;
// Create a DOMPoint for the pixel coordinates
const p = DOMPoint.fromPoint({ x, y });
// Get the inverse of the transformation applied to the canvas context
const t = ctx.getTransform().inverse();
// Use it to calculate context coordinates for your pixel point
const { x: adjustedX, y: adjustedY } = t.transformPoint(p);
Here's it applied to your code.
function showLines() {
console.log(lines)
}
function toggleDrawMode() {
isDrawingMode = !isDrawingMode
}
const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
let lines = [];
const viewportTransform = {
x: 0,
y: 0,
scale: 1
}
// From here on, everything we'll write will go below 👇
const drawRect = (x, y, width, height, color) => {
ctx.fillStyle = color
ctx.fillRect(x, y, width, height)
}
let isDrawingMode = true
const penWidth = 5
const penColor = 'red'
const drawPixels = (x, y) => {
ctx.beginPath()
ctx.arc(x, y, penWidth, 2 * Math.PI, false)
ctx.fillStyle = penColor
ctx.fill()
}
const erasePixels = (x, y) => {
ctx.save()
ctx.beginPath()
ctx.arc(x, y, penWidth, 2 * Math.PI, false)
ctx.globalCompositeOperation = 'destination-out'
ctx.fillStyle = 'white'
ctx.fill()
ctx.restore()
}
const render = () => {
// New code 👇
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.setTransform(viewportTransform.scale, 0, 0, viewportTransform.scale, viewportTransform.x, viewportTransform.y);
// New Code 👆
// drawRect(0, 0, 100, 100, 'red');
// drawRect(200, 200, 100, 100, 'blue');
// drawPixels(400, 400)
// Ok, so we DO need to redraw the lines. That means we DO need to KEEP TRACK of them!
// This drawing just needs absolute figures, the same as they were when the line was being made! The viewportTransform will automatically adjust shit here and there.
lines.forEach((line, idx) => {
// console.log(`letsdraw this line #${idx}`, line, `from the start pos (${previousXDrawing}, ${previousYDrawing})`);
// drawPixels(line.x, line.y)
if (line.isEraseLine) {
eraseLine(line.x, line.y, line.previousX, line.previousY)
} else {
drawLine(line.x, line.y, line.previousX, line.previousY)
}
// TODO: bullshit most likely
// previousXDrawing = line.x
// previousYDrawing = line.y
});
}
render()
// drawPixels(400, 400)
// lines.push({ x: 400, y: 400 })
// We need to keep track of our previous mouse position for later
let previousX = 0,
previousY = 0;
let previousXDrawing = 0,
previousYDrawing = 0;
const updatePanning = (e) => {
const localX = e.clientX;
const localY = e.clientY;
viewportTransform.x += localX - previousX;
viewportTransform.y += localY - previousY;
previousX = localX;
previousY = localY;
}
// If we are trying to zoom out to a level lesser than zoom level 1, then we will not do anything
const isZoomAllowed = (viewportTransform, deltaY) => {
return viewportTransform.scale + deltaY * -0.01 >= 1
}
const updateZooming = (e) => {
const oldScale = viewportTransform.scale;
const oldX = viewportTransform.x;
const oldY = viewportTransform.y;
const localX = e.clientX;
const localY = e.clientY;
const previousScale = viewportTransform.scale;
const newScale = viewportTransform.scale += e.deltaY * -0.01;
const newX = localX - (localX - oldX) * (newScale / previousScale);
const newY = localY - (localY - oldY) * (newScale / previousScale);
viewportTransform.x = newX;
viewportTransform.y = newY;
viewportTransform.scale = newScale;
}
const drawLine = (x, y, previousPosX, previousPosY) => {
ctx.beginPath()
ctx.moveTo(previousPosX, previousPosY)
ctx.lineTo(x, y)
ctx.strokeStyle = penColor
ctx.lineWidth = penWidth
ctx.lineCap = 'round'
ctx.stroke()
}
const eraseLine = (x, y, previousPosX, previousPosY) => {
ctx.save()
ctx.beginPath()
ctx.moveTo(previousPosX, previousPosY)
ctx.lineTo(x, y)
ctx.globalCompositeOperation = 'destination-out'
// ctx.strokeStyle = 'white' // NOT REALLY NEEDED. Destination out means you're just removing the effects of the pen, regardless of whatever colors they were.
ctx.lineWidth = penWidth
ctx.lineCap = 'round'
ctx.stroke()
ctx.restore()
}
const onMouseMove = (e) => {
// Handle the case of drawing here. If drawing mode is on, then that should be handled.
if (isDrawingMode) {
// FINAL: yes this is the final solution! I have got it! Eureka!!!! yippieee
// TODO: change this PART B
console.log('X walay: ', e.offsetX, viewportTransform.scale, viewportTransform.x);
console.log('Y walay: ', e.offsetY, viewportTransform.scale, viewportTransform.y);
const bounding = canvas.getBoundingClientRect();
const x = e.clientX - bounding.left;
const y = e.clientY - bounding.top;
const p = DOMPoint.fromPoint({ x, y });
const t = ctx.getTransform().inverse();
const { x: adjustedX, y: adjustedY } = t.transformPoint(p);
const xToDraw = adjustedX
const yToDraw = adjustedY
if (e.shiftKey) {
// erasePixels(xToDraw, yToDraw)
eraseLine(xToDraw, yToDraw, previousXDrawing, previousYDrawing)
lines.push({
x: xToDraw,
y: yToDraw,
previousX: previousXDrawing,
previousY: previousYDrawing,
isEraseLine: true
}) // TODO: of course we somehow need to remember that this is a erased line. and handle it that way
} else {
// drawPixels(xToDraw, yToDraw)
// drawPixels(400, 400)
drawLine(xToDraw, yToDraw, previousXDrawing, previousYDrawing)
lines.push({
x: xToDraw,
y: yToDraw,
previousX: previousXDrawing,
previousY: previousYDrawing,
isEraseLine: false
})
}
// bogus attempt TODO: problem either here!
previousXDrawing = adjustedX
previousYDrawing = adjustedY
console.log('on moving of mouse, these were the assigned prev x and y: ', previousXDrawing, previousYDrawing);
} else {
updatePanning(e)
render()
}
// updatePanning(e)
// render()
// console.log(e)
}
const onMouseWheel = (e) => {
if (isZoomAllowed(viewportTransform, e.deltaY)) {
updateZooming(e)
render()
}
// console.log(e)
}
canvas.addEventListener("wheel", onMouseWheel);
canvas.addEventListener("mousedown", (e) => {
// This is needed for ensuring smooth panning
previousX = e.clientX;
previousY = e.clientY;
// This is my attempt to make the drawLine logic work TODO: or problem is here
if (isDrawingMode) {
// TODO: change this PARTA
const adjustedX = e.offsetX * viewportTransform.scale - viewportTransform.x;
const adjustedY = e.offsetY * viewportTransform.scale - viewportTransform.y;
// const adjustedX = (e.clientX + viewportTransform.x) * viewportTransform.scale;
// const adjustedY = (e.clientY + viewportTransform.y) * viewportTransform.scale ;
var bounding = canvas.getBoundingClientRect();
var x = e.clientX - bounding.left;
var y = e.clientY - bounding.top;
previousXDrawing = adjustedX
previousYDrawing = adjustedY
// previousXDrawing = e.offsetX
// previousYDrawing = e.offsetY
// TODO: for some reason, over here e.offsetX is a lot.
// maybe we are using the wrong key for comparing x and y coordinates for drawing, and need something else
console.log('in adding event listener, these were prev X and Y: ', previousXDrawing, previousYDrawing)
}
canvas.addEventListener("mousemove", onMouseMove);
})
canvas.addEventListener("mouseup", (e) => {
canvas.removeEventListener("mousemove", onMouseMove);
})
<canvas width="500" height="500" id="canvas" style="border: 2px solid black; background-color: yellow;"></canvas>
<label for="">Drawing Mode</label>
<input type="checkbox" checked onchange="toggleDrawMode()">
<button onclick="showLines()">Show stats of lines</button>
Note: there's a bug drawing a line from some coordinate to the start of the zoomed drawing. Since you posted quite some code I didn't feel like hunting that down. Hopefully you can find it yourself rather quickly.