Related
Here below is how to draw a "selection rectangle" on a <canvas> with drag-and-drop, see How to draw a selection rectangle with drag and drop on a HTML canvas?.
Is there a simple way to detect the selection rectangle on hover at a distance of a few pixels, and allow to move the selection rectangle with drag-and-drop?
var c1 = document.getElementById("c1"), c2 = document.getElementById("c2");
var ctx1 = c1.getContext("2d"), ctx2 = c2.getContext("2d");
ctx2.setLineDash([5, 5]);
var origin = null;
window.onload = () => { ctx1.drawImage(document.getElementById("img"), 0, 0); }
c2.onmousedown = e => { origin = {x: e.offsetX, y: e.offsetY}; };
window.onmouseup = e => { origin = null; };
c2.onmousemove = e => {
if (!!origin) {
ctx2.strokeStyle = "#ff0000";
ctx2.clearRect(0, 0, c2.width, c2.height);
ctx2.beginPath();
ctx2.rect(origin.x, origin.y, e.offsetX - origin.x, e.offsetY - origin.y);
ctx2.stroke();
}
};
#img { display: none; }
#canvas-container { position: relative; }
canvas { position: absolute; left: 0; top: 0; }
#c1 { z-index: 0; }
#c2 { z-index: 1; }
<img id="img" src="https://i.imgur.com/okSIKkW.jpg">
<div id="canvas-container">
<canvas id="c1" height="200" width="200"></canvas>
<canvas id="c2" height="200" width="200"></canvas>
</div>
Storing the current selection
To be able to move an existing selection, you have to save its state. Right now, your code "forgets" about it after drawing it once.
You can save your selection in a variable on mouseup like so:
const dx = origin.x - mouse.x;
const dy = origin.y - mouse.y;
selection = {
left: Math.min(mouse.x, origin.x),
top: Math.min(mouse.y, origin.y),
width: Math.abs(dx),
height: Math.abs(dy)
}
Intersection check
Your selection is a rectangle. You can check whether the mouse is intersecting the rectangle like so:
const intersects = (
mouse.x >= selection.left &&
mouse.x <= selection.left + selection.width &&
mouse.y >= selection.top &&
mouse.y <= selection.top + selection.height
);
If you want to add some padding around the rectangle, you can change the checks to be, for example, mouse.x >= selection.left - PADDING.
Make or Move
You now have to support 2 types of interactions:
Making new selections, and
Moving existing selections
This is where my implementation gets a bit messy, but you can probably refactor it yourself 🙂
I didn't change much in the making-selections code, other than saving the selection to a variable.
When moving selections, you take the dx and dy of your mouse drag and add them to the selection's original position (ox, oy):
const dx = origin.x - mouse.x;
const dy = origin.y - mouse.y;
selection.left = ox - dx;
selection.top = oy - dy;
Here's everything in a runnable snippet:
var c1 = document.getElementById("c1"),
c2 = document.getElementById("c2");
var ctx1 = c1.getContext("2d"),
ctx2 = c2.getContext("2d");
ctx2.setLineDash([5, 5]);
var origin = null;
let selection = null;
let selectionHovered = false;
let interaction = null;
let ox = null;
let oy = null;
window.onload = () => { ctx1.drawImage(document.getElementById("img"), 0, 0); }
c2.onmousedown = e => {
origin = {x: e.offsetX, y: e.offsetY};
if (selectionHovered) {
interaction = "MOVE_SELECTION";
ox = selection.left;
oy = selection.top;
} else {
interaction = "MAKE_SELECTION";
}
};
window.onmouseup = e => {
interaction = null;
origin = null;
};
c2.onmousemove = e => {
const x = e.offsetX;
const y = e.offsetY;
if (!interaction) {
selectionHovered = (
selection &&
x >= selection.left &&
x <= selection.left + selection.width &&
y >= selection.top &&
y <= selection.top + selection.height
);
} else {
const dx = origin.x - x;
const dy = origin.y - y;
// Update
switch (interaction) {
case "MOVE_SELECTION":
selection.left = ox - dx;
selection.top = oy - dy;
break;
case "MAKE_SELECTION":
selection = {
left: Math.min(x, origin.x),
top: Math.min(y, origin.y),
width: Math.abs(dx),
height: Math.abs(dy)
}
break
default:
// Do nothing
}
// Set selectionHovered
if (selection) {
} else {
selectionHovered = false;
}
}
// Draw
if (selection) drawSelection(selection);
};
function drawSelection({ top, left, width, height }) {
// Draw rect
ctx2.strokeStyle = "#ff0000";
ctx2.clearRect(0, 0, c2.width, c2.height);
ctx2.beginPath();
ctx2.rect(left, top, width, height);
ctx2.stroke();
// Set mouse
c2.style.cursor = selectionHovered ? "move" : "default";
}
#img { display: none; }
body { margin: 0; }
#canvas-container { position: relative; }
canvas { position: absolute; left: 0; top: 0; }
#c1 { z-index: 0; }
#c2 { z-index: 1; }
<img id="img" src="https://i.imgur.com/okSIKkW.jpg">
<div id="canvas-container">
<canvas id="c1" height="200" width="200"></canvas>
<canvas id="c2" height="200" width="200"></canvas>
</div>
I've tried for the last few days without too much success to rotate, scale and translate shapes on the canvas.
I've read everything I could find on internet about similar issues but still I cannot seem to be able to adapt it to my own problem.
If everything is drawn on the same scale, I can still drag and drop. If I rotate the shapes, then the mouseOver is messed up since the world coordinates don't correspond anymore with the shape coordinates.
If I scale, then it's impossible to select any shape.
I look at my code and do not understand what I'm doing wrong.
I read some really nice and detailed stackoverflow solutions to similar problems.
For example, user #blindman67 made a suggestion of using a setTransform helper and a getMouseLocal helper for getting the coordinates of the mouse relative to the transformed shape.
inverse transform matrix
I spent some time with that and could not fix my issue.
Here is an example of what I tried. Any suggestion is appreciated.
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth - 40;
canvas.height = window.innerHeight - 60;
const canvasBounding = canvas.getBoundingClientRect();
const offsetX = canvasBounding.left;
const offsetY = canvasBounding.top;
let scale = 1;
let selectedShape = '';
let startX = 0;
let startY = 0;
let endX = 0;
let endY = 0;
let mouseIsDown = false;
let mouseIsMovingShape = false;
let selectedTool = 'SELECT';
let shapes = {};
const selectButton = document.getElementById('select');
const rectangleButton = document.getElementById('rectangle');
canvas.addEventListener('mousedown', canvasMouseDown);
canvas.addEventListener('mouseup', canvasMouseUp);
canvas.addEventListener('mousemove', canvasMouseMove);
function canvasMouseDown(e) {
e.preventDefault();
const mouseX = e.clientX - offsetX;
const mouseY = e.clientY - offsetY;
startX = mouseX;
startY = mouseY;
mouseIsDown = true;
selectedShape = '';
if (selectedTool === 'SELECT') {
for (const shapeId in shapes) {
const shape = shapes[shapeId];
if (shape.mouseIsOver(mouseX, mouseY)) {
selectedShape = shape.id;
shapes[shape.id].isSelected = true;
} else {
shapes[shape.id].isSelected = false;
}
}
}
draw();
}
function canvasMouseUp(e) {
e.preventDefault();
const mouseX = e.clientX - offsetX;
const mouseY = e.clientY - offsetY;
endX = mouseX;
endY = mouseY;
mouseIsDown = false;
const tooSmallShape = Math.abs(endX) - startX < 1 || Math.abs(endY) - startY < 1;
if (tooSmallShape) {
return;
}
if (selectedTool === 'RECTANGLE') {
const newShape = new Shape(selectedTool.toLowerCase(), startX, startY, endX, endY);
shapes[newShape.id] = newShape;
selectedShape = '';
setActiveTool('SELECT');
}
draw();
}
function canvasMouseMove(e) {
e.preventDefault();
const mouseX = e.clientX - offsetX;
const mouseY = e.clientY - offsetY;
const dx = e.movementX;
const dy = e.movementY;
if (mouseIsDown) {
draw();
if (selectedTool === 'SELECT' && selectedShape !== '') {
const shape = shapes[selectedShape];
shape.x += dx;
shape.y += dy;
}
if (selectedTool === 'RECTANGLE') {
drawShapeGhost(mouseX, mouseY);
}
}
}
function draw() {
clear();
for (const shapeId in shapes) {
const shape = shapes[shapeId];
shape.drawShape(ctx);
}
}
function clear() {
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.fillStyle = 'rgba(255, 255, 255, 1)';
ctx.fillRect(0, 0, canvas.width, canvas.height)
}
function drawShapeGhost(x, y) {
ctx.strokeStyle = 'rgba(0, 0, 0, 0.5)';
ctx.strokeRect(startX, startY, x - startX, y - startY);
ctx.stroke();
}
function setActiveTool(tool) {
selectedTool = tool;
if (tool === 'RECTANGLE') {
rectangleButton.classList.add('active');
selectButton.classList.remove('active');
selectedTool = tool;
}
if (tool === 'SELECT') {
rectangleButton.classList.remove('active');
selectButton.classList.add('active');
selectedTool = tool;
}
}
function degreesToRadians(degrees) {
return (Math.PI * degrees) / 180;
};
class Shape {
constructor(shapeType, startX, startY, endX, endY, fill, stroke) {
this.id = shapeType + Date.now();
this.type = shapeType;
this.x = startX;
this.y = startY;
this.width = Math.abs(endX - startX);
this.height = Math.abs(endY - startY);
this.fill = fill || 'rgba(149, 160, 178, 0.8)';
this.stroke = stroke || 'rgba(0, 0, 0, 0.8)';
this.rotation = 0;
this.isSelected = false;
this.scale = 1;
}
drawShape(ctx) {
switch (this.type) {
case 'rectangle':
this._drawRectangle(ctx);
break;
}
}
_drawRectangle(ctx) {
ctx.save();
ctx.scale(this.scale, this.scale);
ctx.translate(this.x + this.width / 2, this.y + this.height / 2);
ctx.rotate(degreesToRadians(this.rotation));
if (this.fill) {
ctx.fillStyle = this.fill;
ctx.fillRect(-this.width / 2, -this.height / 2, this.width, this.height);
}
if (this.stroke !== null) {
ctx.strokeStyle = this.stroke;
ctx.strokeWidth = 1;
ctx.strokeRect(-this.width / 2, -this.height / 2, this.width, this.height);
ctx.stroke();
}
if (this.isSelected) {
ctx.strokeStyle = 'rgba(254, 0, 0, 1)';
ctx.strokeWidth = 1;
ctx.strokeRect(-this.width / 2, -this.height / 2, this.width, this.height)
ctx.stroke();
ctx.closePath();
}
ctx.restore();
}
mouseIsOver(mouseX, mouseY) {
if (this.type === 'rectangle') {
return (mouseX > this.x && mouseX < this.x + this.width && mouseY > this.y && mouseY < this.y + this.height);
}
}
}
const menu = document.getElementById('menu');
const rotation = document.getElementById('rotation');
const scaleSlider = document.getElementById('scale');
menu.addEventListener('click', onMenuClick);
rotation.addEventListener('input', onRotationChange);
scaleSlider.addEventListener('input', onScaleChange);
function onMenuClick(e) {
const tool = e.target.dataset.tool;
if (tool && tool === 'RECTANGLE') {
rectangleButton.classList.add('active');
selectButton.classList.remove('active');
selectedTool = tool;
}
if (tool && tool === 'SELECT') {
rectangleButton.classList.remove('active');
selectButton.classList.add('active');
selectedTool = tool;
}
}
function onRotationChange(e) {
if (selectedShape !== '') {
shapes[selectedShape].rotation = e.target.value;
draw();
}
}
function onScaleChange(e) {
scale = e.target.value;
for (const shapeId in shapes) {
const shape = shapes[shapeId];
shape.scale = scale;
}
draw();
}
function setTransform(ctx, x, y, scaleX, scaleY, rotation) {
const xDx = Math.cos(rotation);
const xDy = Math.sin(rotation);
ctx.setTransform(xDx * scaleX, xDy * scaleX, -xDy * scaleY, xDx * scaleY, x, y);
}
function getMouseLocal(mouseX, mouseY, x, y, scaleX, scaleY, rotation) {
const xDx = Math.cos(rotation);
const xDy = Math.sin(rotation);
const cross = xDx * scaleX * xDx * scaleY - xDy * scaleX * (-xDy) * scaleY;
const ixDx = (xDx * scaleY) / cross;
const ixDy = (-xDy * scaleX) / cross;
const iyDx = (xDy * scaleY) / cross;
const iyDy = (xDx * scaleX) / cross;
mouseX -= x;
mouseY -= y;
const localMouseX = mouseX * ixDx + mouseY * iyDx;
const localMouseY = mouseX * ixDy + mouseY * iyDy;
return {
x: localMouseX,
y: localMouseY,
}
}
function degreesToRadians(degrees) {
return (Math.PI * degrees) / 180
};
function radiansToDegrees(radians) {
return radians * 180 / Math.PI
};
let timer;
function debounce(fn, ms) {
clearTimeout(timer);
timer = setTimeout(() => fn(), ms);
}
canvas {
margin-top: 1rem;
border: 1px solid black;
}
button {
border: 1px solid #adadad;
background-color: transparent;
}
.active {
background-color: lightblue;
}
.menu {
display: flex;
}
.d-flex {
display: flex;
margin-left: 2rem;
}
.rotation input {
margin-left: 1rem;
max-width: 50px;
}
<div id="menu" class="menu">
<button id="select" data-tool="SELECT">select</button>
<button id="rectangle" data-tool="RECTANGLE">rectangle</button>
<div class="d-flex">
<label for="rotation">rotation </label>
<input type="number" id="rotation" value="0">
</div>
<div class="d-flex">
<label for="scale">scale </label>
<input type="range" step="0.1" min="0.1" max="10" value="1" name="scale" id="scale">
</div>
</div>
<canvas id="canvas"></canvas>
If I have time tomorrow I will try to implement the following to your code but I can provide you with a working example of how to get mouse collision precision on a rotated rectangle. I had the same struggle with this and finally found a good explanation and code that I was able to get to work. Check out this website
Now for my own implementation I did not use the method on that website to get my vertices. As you'll see in my code I have a function called updateCorners() in my Square class. I also have objects called this.tl.x and this.tl.y (for each corner).
The formulas are what I use to get vertices of a translated and rotated rectangle and the corner objects are what are used to determine collision. From there I used the distance() function (Pythagorean theorem), the triangleArea() function, and then the clickHit() function which I renamed to collision() and changed some things.
Example in the snippet below
let canvas = document.getElementById("canvas");
let ctx = canvas.getContext("2d");
canvas.width = 400;
canvas.height = 400;
let shapes = [];
let mouse = {
x: null,
y: null
}
canvas.addEventListener('mousemove', e => {
mouse.x = e.x - canvas.getBoundingClientRect().x;
mouse.y = e.y - canvas.getBoundingClientRect().y;
})
class Square {
constructor(x, y, w, h, c) {
this.x = x;
this.y = y;
this.w = w;
this.h = h;
this.c = c;
this.a = 0;
this.r = this.a * (Math.PI/180);
this.cx = this.x + this.w/2;
this.cy = this.y + this.h/2;
//used to track corners
this.tl = {x: 0, y: 0};
this.tr = {x: 0, y: 0};
this.br = {x: 0, y: 0};
this.bl = {x: 0, y: 0};
}
draw() {
ctx.save();
ctx.translate(this.x, this.y)
ctx.rotate(this.r);
ctx.fillStyle = this.c;
ctx.fillRect(-this.w/2,-this.h/2,this.w,this.h);
ctx.restore();
}
updateCorners() {
this.a += 0.1
this.r = this.a * (Math.PI/180);
let cos = Math.cos(this.r);
let sin = Math.sin(this.r)
//updates Top Left Corner
this.tl.x = (this.x-this.cx)*cos - (this.y-this.cy)*sin+(this.cx-this.w/2);
this.tl.y = (this.x-this.cx)*sin + (this.y-this.cy)*cos+(this.cy-this.h/2)
//updates Top Right Corner
this.tr.x = ((this.x+this.w)-this.cx)*cos - (this.y-this.cy)*sin+(this.cx-this.w/2)
this.tr.y = ((this.x+this.w)-this.cx)*sin + (this.y-this.cy)*cos+(this.cy-this.h/2)
//updates Bottom Right Corner
this.br.x = ((this.x+this.w)-this.cx)*cos - ((this.y+this.h)-this.cy)*sin+(this.cx-this.w/2)
this.br.y = ((this.x+this.w)-this.cx)*sin + ((this.y+this.h)-this.cy)*cos+(this.cy-this.h/2)
//updates Bottom Left Corner
this.bl.x = (this.x-this.cx)*cos - ((this.y+this.h)-this.cy)*sin+(this.cx-this.w/2)
this.bl.y = (this.x-this.cx)*sin + ((this.y+this.h)-this.cy)*cos+(this.cy-this.h/2)
}
}
let square1 = shapes.push(new Square(250, 70, 25, 25, 'red'));
let square2 = shapes.push(new Square(175,210, 100, 50, 'blue'));
let square3 = shapes.push(new Square(50,100, 30, 50, 'purple'));
let square4 = shapes.push(new Square(140,120, 120, 20, 'pink'));
//https://joshuawoehlke.com/detecting-clicks-rotated-rectangles/
//pythagorean theorm using built in javascript hypot
function distance(p1, p2) {
return Math.hypot(p1.x-p2.x, p1.y-p2.y);
}
//Heron's formula used to determine area of triangle
//in the collision() function we will break the rectangle into triangles
function triangleArea(d1, d2, d3) {
var s = (d1 + d2 + d3) / 2;
return Math.sqrt(s * (s - d1) * (s - d2) * (s - d3));
}
function collision(mouse, rect) {
//area of rectangle
var rectArea = Math.round(rect.w * rect.h);
// Create an array of the areas of the four triangles
var triArea = [
// mouse posit checked against tl-tr
triangleArea(
distance(mouse, rect.tl),
distance(rect.tl, rect.tr),
distance(rect.tr, mouse)
),
// mouse posit checked against tr-br
triangleArea(
distance(mouse, rect.tr),
distance(rect.tr, rect.br),
distance(rect.br, mouse)
),
// mouse posit checked against tr-bl
triangleArea(
distance(mouse, rect.br),
distance(rect.br, rect.bl),
distance(rect.bl, mouse)
),
// mouse posit checked against bl-tl
triangleArea(
distance(mouse, rect.bl),
distance(rect.bl, rect.tl),
distance(rect.tl, mouse)
)
];
let triArea2 = Math.round(triArea.reduce(function(a,b) { return a + b; }, 0));
if (triArea2 > rectArea) {
return false;
}
return true;
}
function animate() {
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.fillStyle = 'black';
ctx.fillText('x: '+mouse.x+',y: '+mouse.y, 50, 50);
for (let i=0; i< shapes.length; i++) {
shapes[i].draw();
shapes[i].updateCorners();
if (collision(mouse, shapes[i])) {
shapes[i].c = 'red';
} else {
shapes[i].c = 'green'
}
}
requestAnimationFrame(animate)
}
animate();
<canvas id="canvas"></canvas>
I'm sure there's many other ways to do this but this is what I was able to understand and get to work. I haven't really messed with scale so I can't help much there.
UPDATE:
Here is a snippet using the method you wanted. Now you can rotate, scale, and translate and still click inside the shape. Be aware I changed your mouse to a global mouse object vice making it a variable in every mouse... function.
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
canvas.width = window.innerWidth - 40;
canvas.height = window.innerHeight - 60;
const canvasBounding = canvas.getBoundingClientRect();
const offsetX = canvasBounding.left;
const offsetY = canvasBounding.top;
let scale = 1;
let selectedShape = "";
let startX = 0;
let startY = 0;
let endX = 0;
let endY = 0;
let mouseIsDown = false;
let mouseIsMovingShape = false;
let selectedTool = "SELECT";
let localMouse = { x: null, y: null };
let mouse = { x: null, y: null };
let shapes = {};
const selectButton = document.getElementById("select");
const rectangleButton = document.getElementById("rectangle");
canvas.addEventListener("mousedown", canvasMouseDown);
canvas.addEventListener("mouseup", canvasMouseUp);
canvas.addEventListener("mousemove", canvasMouseMove);
function canvasMouseDown(e) {
e.preventDefault();
mouse.x = e.clientX - offsetX;
mouse.y = e.clientY - offsetY;
startX = mouse.x;
startY = mouse.y;
mouseIsDown = true;
selectedShape = "";
if (selectedTool === "SELECT") {
for (const shapeId in shapes) {
const shape = shapes[shapeId];
if (shape.mouseIsOver()) {
selectedShape = shape.id;
shapes[shape.id].isSelected = true;
} else {
shapes[shape.id].isSelected = false;
}
}
}
draw();
}
function canvasMouseUp(e) {
e.preventDefault();
mouse.x = e.clientX - offsetX;
mouse.y = e.clientY - offsetY;
endX = mouse.x;
endY = mouse.y;
mouseIsDown = false;
const tooSmallShape =
Math.abs(endX) - startX < 1 || Math.abs(endY) - startY < 1;
if (tooSmallShape) {
return;
}
if (selectedTool === "RECTANGLE") {
const newShape = new Shape(
selectedTool.toLowerCase(),
startX,
startY,
endX,
endY
);
shapes[newShape.id] = newShape;
selectedShape = "";
setActiveTool("SELECT");
}
draw();
}
function canvasMouseMove(e) {
e.preventDefault();
mouse.x = e.clientX - offsetX;
mouse.y = e.clientY - offsetY;
const dx = e.movementX;
const dy = e.movementY;
if (mouseIsDown) {
draw();
if (selectedTool === "SELECT" && selectedShape !== "") {
const shape = shapes[selectedShape];
shape.x += dx;
shape.y += dy;
}
if (selectedTool === "RECTANGLE") {
drawShapeGhost(mouse.x, mouse.y);
}
}
}
function draw() {
clear();
for (const shapeId in shapes) {
const shape = shapes[shapeId];
shape.drawShape(ctx);
}
}
function clear() {
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.fillStyle = "rgba(255, 255, 255, 1)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
function drawShapeGhost(x, y) {
ctx.strokeStyle = "rgba(0, 0, 0, 0.5)";
ctx.strokeRect(startX, startY, x - startX, y - startY);
ctx.stroke();
}
function setActiveTool(tool) {
selectedTool = tool;
if (tool === "RECTANGLE") {
rectangleButton.classList.add("active");
selectButton.classList.remove("active");
selectedTool = tool;
}
if (tool === "SELECT") {
rectangleButton.classList.remove("active");
selectButton.classList.add("active");
selectedTool = tool;
}
}
function degreesToRadians(degrees) {
return (Math.PI * degrees) / 180;
}
class Shape {
constructor(shapeType, startX, startY, endX, endY, fill, stroke) {
this.id = shapeType + Date.now();
this.type = shapeType;
this.x = startX;
this.y = startY;
this.width = Math.abs(endX - startX);
this.height = Math.abs(endY - startY);
this.fill = fill || "rgba(149, 160, 178, 0.8)";
this.stroke = stroke || "rgba(0, 0, 0, 0.8)";
this.rotation = 0;
this.isSelected = false;
this.scale = { x: 1, y: 1 };
}
drawShape(ctx) {
switch (this.type) {
case "rectangle":
this._drawRectangle(ctx);
break;
}
}
_drawRectangle(ctx) {
ctx.save();
setTransform(
this.x + this.width / 2,
this.y + this.height / 2,
this.scale.x,
this.scale.y,
degreesToRadians(this.rotation)
);
if (this.fill) {
ctx.fillStyle = this.fill;
ctx.fillRect(-this.width / 2, -this.height / 2, this.width, this.height);
}
if (this.stroke !== null) {
ctx.strokeStyle = this.stroke;
ctx.strokeWidth = 1;
ctx.strokeRect(
-this.width / 2,
-this.height / 2,
this.width,
this.height
);
ctx.stroke();
}
if (this.isSelected) {
ctx.strokeStyle = "rgba(254, 0, 0, 1)";
ctx.strokeWidth = 1;
ctx.strokeRect(
-this.width / 2,
-this.height / 2,
this.width,
this.height
);
ctx.stroke();
ctx.closePath();
}
ctx.restore();
}
mouseIsOver() {
localMouse = getMouseLocal(
mouse.x,
mouse.y,
this.x + this.width / 2,
this.y + this.height / 2,
this.scale.x,
this.scale.y,
degreesToRadians(this.rotation)
);
if (this.type === "rectangle") {
if (
localMouse.x > 0 - this.width / 2 &&
localMouse.x < 0 + this.width / 2 &&
localMouse.y < 0 + this.height / 2 &&
localMouse.y > 0 - this.height / 2
) {
return true;
}
}
}
}
const menu = document.getElementById("menu");
const rotation = document.getElementById("rotation");
const scaleSlider = document.getElementById("scale");
menu.addEventListener("click", onMenuClick);
rotation.addEventListener("input", onRotationChange);
scaleSlider.addEventListener("input", onScaleChange);
function onMenuClick(e) {
const tool = e.target.dataset.tool;
if (tool && tool === "RECTANGLE") {
rectangleButton.classList.add("active");
selectButton.classList.remove("active");
selectedTool = tool;
}
if (tool && tool === "SELECT") {
rectangleButton.classList.remove("active");
selectButton.classList.add("active");
selectedTool = tool;
}
}
function onRotationChange(e) {
if (selectedShape !== "") {
shapes[selectedShape].rotation = e.target.value;
draw();
}
}
function onScaleChange(e) {
scale = e.target.value;
for (const shapeId in shapes) {
const shape = shapes[shapeId];
shape.scale.x = scale;
shape.scale.y = scale;
}
draw();
}
function setTransform(x, y, sx, sy, rotate) {
var xdx = Math.cos(rotate); // create the x axis
var xdy = Math.sin(rotate);
ctx.setTransform(xdx * sx, xdy * sx, -xdy * sy, xdx * sy, x, y);
}
function getMouseLocal(mouseX, mouseY, x, y, sx, sy, rotate) {
var xdx = Math.cos(rotate); // create the x axis
var xdy = Math.sin(rotate);
var cross = xdx * sx * xdx * sy - xdy * sx * -xdy * sy;
var ixdx = (xdx * sy) / cross; // create inverted x axis
var ixdy = (-xdy * sx) / cross;
var iydx = (xdy * sy) / cross; // create inverted y axis
var iydy = (xdx * sx) / cross;
mouseX -= x;
mouseY -= y;
var localMouseX = mouseX * ixdx + mouseY * iydx;
var localMouseY = mouseX * ixdy + mouseY * iydy;
return { x: localMouseX, y: localMouseY };
}
function radiansToDegrees(radians) {
return (radians * 180) / Math.PI;
}
let timer;
function debounce(fn, ms) {
clearTimeout(timer);
timer = setTimeout(() => fn(), ms);
}
canvas {
margin-top: 1rem;
border: 1px solid black;
}
button {
border: 1px solid #adadad;
background-color: transparent;
}
.active {
background-color: lightblue;
}
.menu {
display: flex;
}
.d-flex {
display: flex;
margin-left: 2rem;
}
.rotation input {
margin-left: 1rem;
max-width: 50px;
}
<div id="menu" class="menu">
<button id="select" data-tool="SELECT">select</button>
<button id="rectangle" data-tool="RECTANGLE">rectangle</button>
<button id="triangle" data-tool="TRIANGLE">triangle</button>
<div class="d-flex">
<label for="rotation">rotation </label>
<input type="number" id="rotation" value="0">
</div>
<div class="d-flex">
<label for="scale">scale </label>
<input type="range" step="0.1" min="0.1" max="10" value="1" name="scale" id="scale">
</div>
</div>
<canvas id="canvas"></canvas>
I'm making a simple brick breaker game using Java Script, and my console is showing me error while displaying blocks on to the canvas, they are being drawn onto the canvas but all the other objects are not working and console is showing
index.js:173 Uncaught TypeError: Cannot read property 'show' of undefined
at update (index.js:173)
at index.js:177
if you comment out from line no:172 and 173, which is a for loop that tells the canvas to draw them on it
everything's is working fine.
one more thing: that canvasRendering...rundedRectangle is a function that draws rounded edge rectangles
someone please find a solution!
CanvasRenderingContext2D.prototype.roundedRectangle = function(x, y, width, height, rounded) {
const radiansInCircle = 2 * Math.PI
const halfRadians = (2 * Math.PI)/2
const quarterRadians = (2 * Math.PI)/4
// top left arc
this.arc(rounded + x, rounded + y, rounded, -quarterRadians, halfRadians, true)
// line from top left to bottom left
this.lineTo(x, y + height - rounded)
// bottom left arc
this.arc(rounded + x, height - rounded + y, rounded, halfRadians, quarterRadians, true)
// line from bottom left to bottom right
this.lineTo(x + width - rounded, y + height)
// bottom right arc
this.arc(x + width - rounded, y + height - rounded, rounded, quarterRadians, 0, true)
// line from bottom right to top right
this.lineTo(x + width, y + rounded)
// top right arc
this.arc(x + width - rounded, y + rounded, rounded, 0, -quarterRadians, true)
// line from top right to top left
this.lineTo(x + rounded, y)
}
var canvas= document.getElementById("gameCanvas");
var ctx = canvas.getContext("2d");
function Player(x,y,w,h){
this.x = x;
this.y = y;
this.w = w;
this.h = h;
this.show = function(){
ctx.beginPath();
ctx.rect(this.x, this.y, this.w, this.h);
ctx.fillStyle = "#ffff";
ctx.fill();
ctx.closePath();
}
this.move = function(speed){
this.x += speed;
}
}
function Ball(x,y,r){
this.x = x;
this.y = y;
this.r = r;
this.show = function(){
ctx.beginPath();
ctx.arc(this.x,this.y,this.r,0,2* Math.PI);
ctx.fillStyle = "tomato";
ctx.fill();
ctx.closePath();
}
this.move= function(speedX,speedY){
this.show();
this.speed = 2;
this.x += speedX;
this.y += speedY;
}
}
function Block(x,y,w,h){
this.x = x;
this.y = y;
this.w = w;
this.h = h;
this.show= function(color){
ctx.beginPath();
ctx.roundedRectangle(this.x,this.y,this.w,this.h,7);
ctx.fillStyle = color;
ctx.fill();
ctx.closePath();
};
};
var player = new Player(canvas.width/2-50,canvas.height*0.95,100,20);
var ball = new Ball(canvas.width/2-5, player.y-20,15);
var rigthPressed = false;
var leftPressed = false;
var blocks = [];
var rowCount = 5;
var columnCount = 6;
var noInRow = 6;
var blockCount = (rowCount*columnCount)+1;
var blockRow = 0;
var blockCol = 0;
var ballSpeedX = 5;
var ballSpeedY = -10;
for(let i = 1; i < blockCount; i++){
blocks.push(new Block(blockCol*60+25,blockRow*60+30,50,50));
blockCol++;
if(i % noInRow == 0){
blockRow++;
blockCol = 0;
}
}
window.addEventListener("keydown", function(e){
if(e.keyCode == 39){
rigthPressed = true;
}
if(e.keyCode == 37){
leftPressed = true;
}
});
window.addEventListener("keyup", function(e){
if(e.keyCode == 39){
rigthPressed = false;
}
if(e.keyCode == 37){
leftPressed = false;
}
});
function objMovement(){
if(rigthPressed){
player.move(5);
if (player.x > canvas.width-player.w){
player.x = canvas.width-player.w;
}
}
if(leftPressed){
player.move(-5);
if(player.x < 0){
player.x = 0;
}
}
if(ball.x > canvas.width-ball.r || ball.x < 0+ball.r){
ballSpeedX = -ballSpeedX;
}
if (ball.y > canvas.height-ball.r || ball.y < 0+ball.r){
ballSpeedY = -ballSpeedY;
}
if(ball.x<player.x+player.w &&ball.x>player.x && ball.y>player.y){
ballSpeedY = -ballSpeedY;
ballSpeedX= ballSpeedX;
}
function Bump(){
if (ball.x>player.x && ball.x<player.x+player.w/2){
if (ball.y >= player.y){
ballSpeedX = -5;
}
}
if(ball.x>player.x+player.w/2 && ball.x<player.x+player.w){
if(ball.y >= player.y){
ballSpeedX = 5;
}
}
}
//Bump();
}
function update(){
ctx.clearRect(0,0,canvas.width,canvas.height);
ball.show();
ball.move(ballSpeedX,ballSpeedY);
player.show();
objMovement();
for(let i=0;i<blockCount;i++){
blocks[i].show("violet");
}
window.requestAnimationFrame(update);
}
update();
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>🌝🩲🩲🌝</title>
<style>
#body{
background-color: rgb(31, 30, 30);
}
#gameCanvas{
border: 15px solid rgb(44, 44, 44);
border-radius: 20px;
background-color:rgb(19, 18, 18);
margin: 250px;
}
</style>
</head>
<body id="body">
<canvas id="gameCanvas" width=400 height=800></canvas>
<script type="text/javascript" src="./index.js"></script>
</body>
</html>
When you create blocks you start from 1
for (let i = 1; i < blockCount; i++) {
blocks.push(new Block(blockCol * 60 + 25, blockRow * 60 + 30, 50, 50));
...
so when you update you need to consider that
function update() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ball.show();
ball.move(ballSpeedX, ballSpeedY);
player.show();
objMovement();
for (let i = 0; i < blockCount -1; i++) { // or for (let i = 0; i < blocks.length; i++) {
blocks[i].show("violet");
}
window.requestAnimationFrame(update);
}
I need help with a pretty difficult problem. I am currently making a game with React and Redux. In this game I use a canvas to create a map data from my redux store. Currently the map is just a matrix of black and white squares. Let's say that I wanted to change the color of a square when you click on it, but also maintain the ability to drag the element. The problem is that it is difficult to pinpoint exactly where the mouse is clicking given that the element can be dragged anywhere on the page. As far as I can tell, none of the mouseclick event object properties seem to be able to tell me. I thought offsetX and offsetY would tell me, but they don't seem to stay the same when the canvas object moves for some reason.
I am using React and Redux for this project, and the CanvasMap element is wrapped in a Draggable from this library: https://www.npmjs.com/package/react-draggable#draggablecore
import React from 'react'
import { connect } from 'react-redux'
import './CanvasMap.css'
class CanvasMap extends React.Component{
componentDidMount() {
const map = this.props.map //Data representing the map
const canvas = this.refs.canvas
const context = canvas.getContext('2d')
//Building the map on the canvas
for(let i = 0; i < map.length; i++){
for(let j = 0; j < map[i].length; j++){
const x=i*100
const y=j*100
const isBlackSquare= map[i][j] === 'black' ? true : false
if(isBlackSquare) context.fillRect(x, y, 100, 100)
else context.strokeRect(x, y, 100, 100)
}
}
function handleClick(e){
//None of the event properties seem to stay the same when the canvas is moved
console.log(e)
}
canvas.addEventListener('click', handleClick)
}
render(){
return (
<div className='Map'>
<canvas ref='canvas' width={8000} height={8000} />
</div>
)
}
}
function mapStateToProps(state){
return {
map: [...state.map]
}
}
const connectedComponent = connect(mapStateToProps)(CanvasMap)
export { connectedComponent as CanvasMap }
In most cases when you click on an HTML element you can use the rectangleBounding Box and get coordinators from that like
domRect = element.getBoundingClientRect();
in a canvas click position it is a little more difficult
Here is a script I did a while ago to draw while dragging the mouse the on the canvas .Maybe you can apply this method
<html>
<head>
<style>
* { margin:0; padding:0; } /* to remove the top and left whitespace */
html, body { width:100%; height:100%; } /* just to be sure these are full screen*/
canvas { display:block; } /* To remove the scrollbars */
</style>
</head>
<body>
<canvas id="canvas" ></canvas>
<script>
////////////////////////////////////////
(function() {
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var elemLeft = canvas.offsetLeft;
var elemTop = canvas.offsetTop;
var BB=canvas.getBoundingClientRect();
var offsetX=BB.left;
var offsetY=BB.top;
// resize the canvas to fill browser window dynamically
window.addEventListener('resize', resizeCanvas, false);
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
/**
* Your drawings need to be inside this function otherwise they will be reset when
* you resize the browser window and the canvas goes will be cleared.
*/
drawStuff();
}
resizeCanvas();
function drawStuff() {
// do your drawing stuff here
var img = new Image();
img.src = 'images/3PkBe.gif';
img.onload = function()
{
//var canvas = document.getElementById('canvas');
// create pattern
var ptrn = ctx.createPattern(img, 'repeat'); // Create a pattern with this image, and set it to "repeat".
ctx.fillStyle = ptrn;
ctx.fillRect(0, 0, canvas.width, canvas.height); // context.fillRect(x, y, width, height);
ctx.shadowBlur=20;
//ctx.shadowColor="black";
//ctx.fillStyle="green";
//ctx.fillRect(20,160,100,80);
ctx.strokeStyle = "lightgray";
//var canvasOffset = canvas.offset();
//var offsetX = canvasOffset.left;
//var offsetY = canvasOffset.top;
var mouseIsDown = false;
var lastX = 0;
var lastY = 0;
var elements = [];
makeShip( 30 , 30,120, 120, '#119' , "romea");
makeShip( 30, 160,120, 120, '#393', "fomar");
makeShip( 30, 290,120, 120, '#955', "ojab");
makeShip( 30, 420,120, 120, '#6ff', "eliot");
// Add event listener for `click` events.
canvas.addEventListener('click', function(event) {
var x = event.pageX - elemLeft,
y = event.pageY - elemTop;
console.info(x, y);
elements.forEach(function(element) {
if (y > element.y && y < element.y + element.height && x > element.x && x < element.x + element.width) {
console.log(element.name);
}
});
}, false);
canvas.addEventListener('mousedown', function(event) {
var x = event.pageX - elemLeft,
y = event.pageY - elemTop;
console.info(x, y);
elements.forEach(function(element) {
if (y > element.y && y < element.y + element.height && x > element.x && x < element.x + element.width) {
console.info(element.name);
handleMouseDown(element);
}
});
}, false);
canvas.addEventListener('mousemove', function(event) {
var x = event.pageX - elemLeft,
y = event.pageY - elemTop;
console.info(x, y);
elements.forEach(function(element) {
if (y > element.y && y < element.y + element.height && x > element.x && x < element.x + element.width) {
console.info(element.name);
handleMouseMove(element,x,y);
}
});
}, false);
canvas.addEventListener('mouseup', function(event) {
var x = event.pageX - elemLeft,
y = event.pageY - elemTop;
//console.info(x, y);
elements.forEach(function(element) {
//if (y > element.y && y < element.y + element.height && x > element.x && x < element.x + element.width) {
console.info(element.name + "mouse up evenr=========");
handleMouseUp(element);
//}
});
}, false);
function makeShip(x, y, width, height, colour,ShipName) {
var ship = {
name: ShipName,
colour: colour,
width: width,
height: height,
x: x,
y: y
}
elements.push(ship);
return (ship);
}
function drawShip(ship) {
//ctx.fillStyle = ship.colour;
//ctx.fillRect(ship.x, ship.y, ship.width, ship.height);
//ctx.fillRect(element.x, element.y, element.width, element.height);
}
function drawAllShips() {
// ctx.clearRect(0, 0, canvas.width, canvas.height);
for (var i = 0; i < elements.length; i++) {
var ship = elements[i]
//drawShip(ship);
ctx.fillStyle = ship.colour;
ctx.fillRect(ship.x , ship.y, ship.width, ship.height);
// ctx.fillStyle = ship.fill;
// ctx.fill();
// ctx.stroke();
}
}
// Add element.
//elements.push({
//colour: '#05EFFF',
//width: 150,
//height: 100,
//x: 20,
//y: 15
//});
// Render elements.
// elements.forEach(function(element) {
// ctx.fillStyle = element.colour;
// ctx.fillRect(element.x, element.y, element.width, element.height);
// });
drawAllShips();
function handleMouseDown(e) {
mouseX = e.x ;
mouseY = e.y ;
//mouseX = parseInt(e.x - offsetX);
//mouseY = parseInt(e.y - offsetY);
console.log("===========Problem "+mouseX);
// mousedown stuff here
lastX = mouseX;
lastY = mouseY;
mouseIsDown = true;
//alert("mouse Handle");
}
function handleMouseUp(e) {
//mouseX = parseInt(e.clientX - offsetX);
//mouseY = parseInt(e.clientY - offsetY);
ctx.onmousemove = null;
// mouseup stuff here
mouseIsDown = false;
return
}
function handleMouseMove(e,x,y) {
if (mouseIsDown) {
//console.log(' no fuck');
mouseX = e.x ;
mouseY = e.y ;
console.log(e.name+"is truing to drag");
// mousemove stuff here
//for (var i = 0; i < elements.length; i++) {
//if (ctx.isPointInPath(mouseX, mouseY)) {
//console.log('============== no fuck');
var ship =e;// elements[i];
ship.x = x-15;//(mouseX - lastX);
ship.y = y-20;//(mouseY -lastY);
// ship.right = ship.x + ship.width;
// ship.bottom = ship.y + ship.height;
//drawShip(ship);
//}
//}
lastX = mouseX;
lastY = mouseY;
drawAllShips();
}
}
<!-- ctx.mousedown(function (e) { -->
<!-- handleMouseDown(e); -->
<!-- }); -->
<!-- ctx.mousemove(function (e) { -->
<!-- handleMouseMove(e); -->
<!-- }); -->
<!-- ctx.mouseup(function (e) { -->
<!-- handleMouseUp(e); -->
<!-- }); -->
}
}
})();
</script>
</body>
</html>
SOLVED IT!
I got lost in the slew of data around elements and click events, I was trying to figure out the right combination of pageX, clientX, offsetLeft, screenX, etc. However, the final solution is incredibly simple once you know exactly what to do. Here it is:
function handleClick(e){
const rect = e.target.getBoundingClientRect()
const x = e.pageX - rect.left
const y = e.pageY - rect.top
}
This should get you the exact x and y coordinate of your mouse in relation to the element, no matter where you drag and reposition the element.
I need to add text to the canvas and move the whole canvas then.
When I'm just adding the text everything works fine.
But when I try to move the canvas the text is moving strange.
As you can see, when I'm moving the canvas the text is moving not like it supposed to be.
How can I fix it?
function renderTextCanvas() {
ctx.clearRect(0, 0, img.width, img.height);
ctx.scale(scale, scale);
ctx.drawImage(img, 0, 0, canvas.width, canvas.height, viewportOffsetX, viewportOffsetY, img.width, img.height);
textComponents.forEach(function (textObj) {
ctx.save();
var offsetX = textObj.x;
var offsetY = textObj.y;
ctx.translate(offsetX, offsetY);
ctx.rotate(textObj.rotation);
ctx.translate(-offsetX, -offsetY);
ctx.font = textObj.fontSize + 'px ' + textObj.font;
ctx.fillStyle = textObj.color;
if (textObj.saved) {
ctx.fillText(textObj.text, textObj.x + viewportOffsetX, textObj.y + viewportOffsetY);
}
else {
ctx.fillText(textObj.text, textObj.x, textObj.y);
textObj.saved = true;
textObj.x -= viewportOffsetX;
textObj.y -= viewportOffsetY;
}
ctx.restore();
});
Viewport offsets are being changed on drag:
function onMouseMove(e) {
if (drag) {
var pageX = e.pageX;
var pageY = e.pageY;
if (e.touches) {
var touches = e.touches[0];
pageX = touches.pageX;
pageY = touches.pageY;
}
var deltaX = pageX - originX;
var deltaY = pageY - originY;
if (Math.abs(viewportOffsetX) < Math.abs(img.width / 2)) {
viewportOffsetX += deltaX;
}
else {
if (viewportOffsetX < 0) {
viewportOffsetX += 1;
}
else {
viewportOffsetX -= 1;
}
}
if (Math.abs(viewportOffsetY) < Math.abs(img.height / 2)) {
viewportOffsetY += deltaY;
}
else {
if (viewportOffsetY < 0) {
viewportOffsetY += 1;
}
else {
viewportOffsetY -= 1;
}
}
originX = pageX;
originY = pageY;
renderTextCanvas();
}
}