Related
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>
what exactly I want to achieve is
to draw objects on canvas and
on mouseover display relevant data in tooltip.
here you can view the code.
var canvasBack;
var canvasLabel;
var canvasDraw;
var ctxBack;
var ctxLabel;
var ctxDraw;
var last_mousex = 0;
var last_mousey = 0;
var mousex = 0;
var mousey = 0;
var canWidth;
var canHeight;
var scaleParameter;
var radius;
var xVertex;
var yVertex;
var hotspots = [];
// initialization on loading of canvas
$('canvas').ready(function() {
init();
});
// initialization function used for binding events, and inital logic implemented.
function init() {
scaleParameter = 1;
canvasBack = document.getElementById('backSpace');
canvasLabel = document.getElementById('layerCanvas');
canvasDraw = document.getElementById('drawSpace');
ctxBack = canvasBack.getContext('2d');
ctxLabel = canvasLabel.getContext('2d');
ctxDraw = canvasDraw.getContext('2d');
canWidth = parseInt($(canvasBack).attr('width'));
canHeight = parseInt($(canvasBack).attr('height'));
var canvasx = $(canvasBack).offset().left;
var canvasy = $(canvasBack).offset().top
var mousedown = false;
//Mousedown
$('canvas').on('mousedown', function(e) {
$('#drawSpace').css('display', 'block');
last_mousex = mousex = parseInt(e.clientX - canvasx);
last_mousey = mousey = parseInt(e.clientY - canvasy);
mousedown = true;
});
//Mouseup
$('canvas').on('mouseup', function(e) {
hotspots.push({
x: xVertex,
y: yVertex,
radius: radius,
tip: 'You are over ' + mousex + ',' + mousey
});
let cw = canvasBack.width;
let ch = canvasBack.height;
ctxBack.drawImage(canvasDraw, 0, 0, cw, ch);
$('#drawSpace').css('display', 'none');
mousedown = false;
});
//Mousemove
$('canvas').on('mousemove', function(e) {
mousex = parseInt(e.clientX - canvasx);
mousey = parseInt(e.clientY - canvasy);
if (mousedown) {
// draw(mousedown);
drawEllipse(last_mousex, last_mousey, mousex, mousey);
} else {
hoverTooltip();
}
});
}
function drawEllipse(x1, y1, x2, y2) {
var leftScroll = $("#scrollParent").scrollLeft();
var topScroll = $("#scrollParent").scrollTop();
let cw = canvasBack.width;
let ch = canvasBack.height;
ctxDraw.clearRect(0, 0, cw, ch);
var radiusX = x2 - x1,
radiusY = y2 - y1,
centerX = x1 + radiusX,
centerY = y1 + radiusY,
step = 0.01,
a = step,
pi2 = Math.PI * 2 - step;
radius = Math.sqrt(radiusX * radiusX + radiusY * radiusY) / 2;
ctxDraw.beginPath();
ctxDraw.arc(centerX, centerY, radius, 0, 2 * Math.PI, true);
ctxDraw.closePath();
ctxDraw.fillStyle = 'green';
ctxDraw.fill();
ctxDraw.strokeStyle = '#000';
ctxDraw.stroke();
xVertex = centerX;
yVertex = centerY;
}
// tooltip show on hover over objects
function hoverTooltip() {
var leftScroll = $("#scrollParent").scrollLeft();
var topScroll = $("#scrollParent").scrollTop();
let cw = canvasBack.width;
let ch = canvasBack.height;
for (var i = 0; i < hotspots.length; i++) {
var h = hotspots[i];
var dx = mousex - h.x;
var dy = mousey - h.y;
if (dx * dx + dy * dy < h.radius * h.radius) {
$('#console').text(h.tip);
ctxLabel.clearRect(0, 0, cw, ch);
ctxLabel.fillText(h.tip, mousex + leftScroll, mousey + topScroll);
} else {
ctxLabel.clearRect(0, 0, cw, ch);
}
}
}
#scrollParent {
width: 644px;
height: 364px;
overflow: auto;
position: relative;
}
#scrollParent>canvas {
position: absolute;
left: 0;
top: 0;
border: 1px solid #ababab;
}
#backSpace {
z-index: 0;
}
#drawSpace {
display: none;
z-index: 1;
}
#layerCanvas {
z-index: 2;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="scrollParent">
<!-- actual canvas that is visible -->
<canvas width="640" height="360" id="backSpace"></canvas>
<!-- canvas used for drawing new objects -->
<canvas width="640" height="360" id="drawSpace"></canvas>
<!-- canvas used to display tooltip -->
<canvas width="640" height="360" id="layerCanvas"></canvas>
</div>
<div id="console"></div>
</div>
actual problem is in the bellow image, tooltip worked fine when the 1st object was drawn, but once the second object was drawn tooltip worked only for the second one, not for previously drawn objects.
what is causing this issue, and how to fix it ?
Removing else will not remove the label when leaving the elipse.
You need to exit the loop once you found the correct elipse from the array using break.
function hoverTooltip() {
var leftScroll = $("#scrollParent").scrollLeft();
var topScroll = $("#scrollParent").scrollTop();
let cw = canvasBack.width;
let ch = canvasBack.height;
for (var i = 0; i < hotspots.length; i++) {
var h = hotspots[i];
var dx = mousex - h.x;
var dy = mousey - h.y;
if (dx * dx + dy * dy < h.radius * h.radius) {
$('#console').text(h.tip);
ctxLabel.clearRect(0, 0, cw, ch);
ctxLabel.fillText(h.tip, mousex + leftScroll, mousey + topScroll);
break; // exit the loop
} else {
ctxLabel.clearRect(0, 0, cw, ch);
}
}
}
UPDATE
I figured that if you draw two objects over each other, it will behave poorly. Try this instead. It will display information of the latest drawn spot.
function hoverTooltip() {
var leftScroll = $("#scrollParent").scrollLeft();
var topScroll = $("#scrollParent").scrollTop();
let cw = canvasBack.width;
let ch = canvasBack.height;
var spots = hotspots.filter((h) => {
var dx = mousex - h.x;
var dy = mousey - h.y;
return (dx * dx + dy * dy < h.radius * h.radius);
})
if (spots.length > 0) {
var h = spots[spots.length - 1]; // latest drawn spot
$('#console').text(h.tip);
ctxLabel.clearRect(0, 0, cw, ch);
ctxLabel.fillText(h.tip, mousex + leftScroll, mousey + topScroll);
}
else
{
ctxLabel.clearRect(0, 0, cw, ch);
}
}
I see there are already a few answers. This is mine:
In order to be able to show the label for every circle on hover you need to save all your circles in am array: the circles array. I'm using the ctx.isPointInPath() method to know if the mouse is over the circle, and if it is I paint the label.
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
let cw = canvas.width = 640;
let ch = canvas.height = 360;
let found = false;//is a circle found?
const cText = document.querySelector("#text");
const ctxText = cText.getContext("2d");
cText.width = 640;
cText.height = 360;
ctxText.font="1em Verdana";
let drawing = false;
let circles = []
class Circle{
constructor(x,y){
this.x = x;
this.y = y;
this.r = 0;
}
updateR(m) {
this.r = dist(this,m);
}
draw(){
ctx.beginPath();
ctx.arc(this.x,this.y,this.r,0,2*Math.PI);
}
paint(){
ctx.fillStyle = "green";
ctx.strokeStyle = "black";
this.draw();
ctx.stroke();
ctx.fill();
}
label(m){
this.draw();
if (ctx.isPointInPath(m.x, m.y)) {
ctx.beginPath();
ctx.arc(this.x, this.y, 4, 0, 2 * Math.PI);
ctxText.fillStyle = "black";
ctxText.fillText(`you are over ${this.x},${this.y}`,m.x,m.y)
found = true;
}
}
}
let m = {}// mouse
cText.addEventListener("mousedown",(e)=>{
drawing = true;
m = oMousePos(canvas, e);
let circle = new Circle(m.x,m.y)
circles.push(circle);
})
cText.addEventListener("mouseup",(e)=>{
drawing = false;
})
cText.addEventListener("mousemove",(e)=>{
m = oMousePos(canvas, e);
found = false;
if(drawing){
let circle = circles[circles.length-1];//the last circle in the circles arrey
circle.updateR(m);
}
ctx.clearRect(0,0, cw,ch);
ctxText.clearRect(0,0,cw,ch)
circles.map((c) => {c.paint();});
for(let i = circles.length-1; i >=0 ; i--){
circles[i].label(m);
if(found){break;}
}
})
function oMousePos(canvas, evt) {
var ClientRect = canvas.getBoundingClientRect();
return { //objeto
x: Math.round(evt.clientX - ClientRect.left),
y: Math.round(evt.clientY - ClientRect.top)
}
}
function dist(p1, p2) {
let dx = p2.x - p1.x;
let dy = p2.y - p1.y;
return Math.sqrt(dx * dx + dy * dy);
}
canvas{border:1px solid;position:absolute; top:0; left:0;}
#scrollParent{position:relative;}
<div id="scrollParent">
<!-- actual canvas that is visible -->
<canvas width="640" height="360"></canvas>
<canvas width="640" height="360" id="text"></canvas>
</div>
I've updated the code in base of the comment of #HelderSepu
A SECOND UPDATE in base of a second message from #HelderSepu. He wants to see "multiple message but avoid overlapping the messages"
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
let cw = canvas.width = 640;
let ch = canvas.height = 360;
let text = "";
const cText = document.querySelector("#text");
const ctxText = cText.getContext("2d");
cText.width = 640;
cText.height = 360;
ctxText.font="1em Verdana";
let drawing = false;
let circles = []
class Circle{
constructor(x,y){
this.x = x;
this.y = y;
this.r = 0;
}
updateR(m) {
this.r = dist(this,m);
}
draw(){
ctx.beginPath();
ctx.arc(this.x,this.y,this.r,0,2*Math.PI);
}
paint(){
ctx.fillStyle = "green";
ctx.strokeStyle = "black";
this.draw();
ctx.stroke();
ctx.fill();
}
label(m){
this.draw();
if (ctx.isPointInPath(m.x, m.y)) {
this.text = `[${this.x},${this.y}]`
}else{
this.text = "";
}
}
}
let m = {}// mouse
cText.addEventListener("mousedown",(e)=>{
drawing = true;
m = oMousePos(canvas, e);
let circle = new Circle(m.x,m.y)
circles.push(circle);
})
cText.addEventListener("mouseup",(e)=>{
drawing = false;
})
cText.addEventListener("mousemove",(e)=>{
m = oMousePos(canvas, e);
if(drawing){
let circle = circles[circles.length-1];//the last circle in the circles arrey
circle.updateR(m);
}
ctx.clearRect(0,0, cw,ch);
ctxText.clearRect(0,0,cw,ch);
text="";
circles.map((c) => {c.paint();c.label(m);});
circles.map((c) => {text += c.text;});
ctxText.fillStyle = "black";
ctxText.fillText(text,m.x,m.y)
})
function oMousePos(canvas, evt) {
var ClientRect = canvas.getBoundingClientRect();
return { //objeto
x: Math.round(evt.clientX - ClientRect.left),
y: Math.round(evt.clientY - ClientRect.top)
}
}
function dist(p1, p2) {
let dx = p2.x - p1.x;
let dy = p2.y - p1.y;
return Math.sqrt(dx * dx + dy * dy);
}
canvas{border:1px solid;position:absolute; top:0; left:0;}
#scrollParent{position:relati
<div id="scrollParent">
<!-- actual canvas that is visible -->
<canvas width="640" height="360"></canvas>
<canvas width="640" height="360" id="text"></canvas>
<div id="console"></div>
</div>
Currently attempting to make a physics simulation for elastic collisions of circles. I am having an issue where I do not know how to run the simulation with two circles interacting at the same time. I am not yet looking to create the interaction between the circles just to have them both running simultaneously. Any help is much appreciated. This is my first post so I apologize if I formatted something incorrectly.
var width = 400;
var height = 400;
var canvas = ctx = false;
var frameRate = 1 / 60; // Seconds
var frameDelay = frameRate * 1000; // ms
var loopTimer = false;
var ball = {
position: {
x: width / 2,
y: height / 2
},
velocity: {
x: 0,
y: 0
},
radius: 15, // 1px = 1cm
restitution: -1
};
var mouse = {
x: 0,
y: 0,
isDown: false
};
function getMousePosition(event) {
mouse.x = event.pageX - canvas.offsetLeft;
mouse.y = event.pageY - canvas.offsetTop;
}
var mouseDown = function(event) {
if (event.which == 1) {
getMousePosition(event);
mouse.isDown = true;
ball.position.x = mouse.x;
ball.position.y = mouse.y;
}
}
var mouseUp = function(event) {
if (event.which == 1) {
mouse.isDown = false;
ball.velocity.y = (ball.position.y - mouse.y) / 10;
ball.velocity.x = (ball.position.x - mouse.x) / 10;
}
}
var setup = function() {
canvas = document.getElementById("canvas");
ctx = canvas.getContext("2d");
canvas.onmousemove = getMousePosition;
canvas.onmousedown = mouseDown;
canvas.onmouseup = mouseUp;
ctx.fillStyle = 'blue';
ctx.strokeStyle = '#000000';
loopTimer = setInterval(loop, frameDelay);
}
var loop = function() {
if (!mouse.isDown) {
ball.position.x += ball.velocity.x * frameRate * 100;
ball.position.y += ball.velocity.y * frameRate * 100;
}
if (ball.position.y > height - ball.radius) {
ball.velocity.y *= ball.restitution;
ball.position.y = height - ball.radius;
}
if (ball.position.x > width - ball.radius) {
ball.velocity.x *= ball.restitution;
ball.position.x = width - ball.radius;
}
if (ball.position.x < ball.radius) {
ball.velocity.x *= ball.restitution;
ball.position.x = ball.radius;
}
if (ball.position.y < ball.radius) {
ball.velocity.y *= ball.restitution;
ball.position.y = ball.radius;
}
ctx.clearRect(0, 0, width, height);
ctx.save();
ctx.translate(ball.position.x, ball.position.y);
ctx.beginPath();
ctx.arc(0, 0, ball.radius, 0, Math.PI * 2, true);
ctx.fill();
ctx.closePath();
ctx.restore();
if (mouse.isDown) {
ctx.beginPath();
ctx.moveTo(ball.position.x, ball.position.y);
ctx.lineTo(mouse.x, mouse.y);
ctx.stroke();
ctx.closePath();
}
}
setup();
#canvas {
border: solid 1px #ccc;
}
<canvas id="canvas"></canvas>
Here is how I would do it:
Instead of making the ball a kind of static object I made a constructor function (More about that here).
Then I made a ball array to store all the balls.
To make the dragging possible I store a seperate ball, which is not being moved by "physics" in the newBall variable. This ball is either invisible or is the ball currently being dragged.
In mouseDown() the newBall gets positioned under the cursor.
In mouseUp() it gets it's velocity and gets added to the array of animated balls. Also a new newBall gets created.
In loop() I loop two times through the array of animated balls. Once for the physics, once for the painting.
(Normally you would use two different methods with different tickRates to make animation more smooth, because physics calculation doesn't need to happen 60 times per second.
var width = 400;
var height = 400;
var canvas = ctx = false;
var frameRate = 1 / 60; // Seconds
var frameDelay = frameRate * 1000; // ms
var loopTimer = false;
function ball() {
this.position = {
x: width / 2,
y: height / 2
};
this.velocity = {
x: 0,
y: 0
};
this.radius = 15; // 1px = 1cm
this.restitution = -1
};
var balls = [];
var newBall = new ball();
var mouse = {
x: 0,
y: 0,
isDown: false
};
function getMousePosition(event) {
mouse.x = event.pageX - canvas.offsetLeft;
mouse.y = event.pageY - canvas.offsetTop;
}
var mouseDown = function(event) {
if (event.which == 1) {
getMousePosition(event);
mouse.isDown = true;
newBall.position.x = mouse.x;
newBall.position.y = mouse.y;
}
}
var mouseUp = function(event) {
if (event.which == 1) {
mouse.isDown = false;
newBall.velocity.y = (newBall.position.y - mouse.y) / 10;
newBall.velocity.x = (newBall.position.x - mouse.x) / 10;
balls.push(newBall);
newBall = new ball();
}
}
var setup = function() {
canvas = document.getElementById("canvas");
ctx = canvas.getContext("2d");
canvas.onmousemove = getMousePosition;
canvas.onmousedown = mouseDown;
canvas.onmouseup = mouseUp;
ctx.fillStyle = 'blue';
ctx.strokeStyle = '#000000';
loopTimer = setInterval(loop, frameDelay);
}
var loop = function() {
for (var ball of balls) {
ball.position.x += ball.velocity.x * frameRate * 100;
ball.position.y += ball.velocity.y * frameRate * 100;
if (ball.position.y > height - ball.radius) {
ball.velocity.y *= ball.restitution;
ball.position.y = height - ball.radius;
}
if (ball.position.x > width - ball.radius) {
ball.velocity.x *= ball.restitution;
ball.position.x = width - ball.radius;
}
if (ball.position.x < ball.radius) {
ball.velocity.x *= ball.restitution;
ball.position.x = ball.radius;
}
if (ball.position.y < ball.radius) {
ball.velocity.y *= ball.restitution;
ball.position.y = ball.radius;
}
}
ctx.clearRect(0, 0, width, height);
for (var ball of balls) {
ctx.save();
ctx.translate(ball.position.x, ball.position.y);
ctx.beginPath();
ctx.arc(0, 0, ball.radius, 0, Math.PI * 2, true);
ctx.fill();
ctx.closePath();
ctx.restore();
}
ctx.save();
ctx.translate(newBall.position.x, newBall.position.y);
ctx.beginPath();
ctx.arc(0, 0, newBall.radius, 0, Math.PI * 2, true);
ctx.fill();
ctx.closePath();
ctx.restore();
if (mouse.isDown) {
ctx.beginPath();
ctx.moveTo(newBall.position.x, newBall.position.y);
ctx.lineTo(mouse.x, mouse.y);
ctx.stroke();
ctx.closePath();
}
}
setup();
#canvas {
border: solid 1px #ccc;
}
<canvas id="canvas"></canvas>
Now to get a bit more complex:
I added tickDelay and tickTimer to use them in a tickLoop
The ball constructor now has two methods:
show() draws the ball on the canvas
tick() does the pysics stuff (dt= deltaTime: time since last tick)
newBall is now null if the mouse isn't pressed
setup() initializes the width and height according to the <canvas> elements real size
tick() loops through the balls and calls .tick() tickDelay is in milliseconds so it gets divided by 1000
drawFrame() is your former loop() and does the drawing stuff
var width = 400;
var height = 400;
var canvas = ctx = false;
var frameRate = 1 / 60; // Seconds
var frameDelay = frameRate * 1000; // ms
var tickDelay = frameDelay * 2; //ticks 2 times slower than frames
var frameTimer;
var tickTimer;
function ball() {
this.position = {
x: width / 2,
y: height / 2
};
this.velocity = {
x: 0,
y: 0
};
this.radius = 15; // 1px = 1cm
this.restitution = -.99;
this.show = function() {
ctx.save();
ctx.translate(this.position.x, this.position.y);
ctx.beginPath();
ctx.arc(0, 0, this.radius, 0, Math.PI * 2, true);
ctx.fill();
ctx.closePath();
ctx.restore();
};
this.tick = function(dt) {
this.position.x += this.velocity.x * dt;
this.position.y += this.velocity.y * dt;
if (this.position.y > height - this.radius) {
this.velocity.y *= this.restitution;
this.position.y = height - this.radius;
}
if (this.position.x > width - this.radius) {
this.velocity.x *= this.restitution;
this.position.x = width - this.radius;
}
if (this.position.x < this.radius) {
this.velocity.x *= this.restitution;
this.position.x = this.radius;
}
if (this.position.y < this.radius) {
this.velocity.y *= this.restitution;
this.position.y = this.radius;
}
}
};
var balls = [];
var newBall;
var mouse = {
x: 0,
y: 0,
isDown: false
};
function getMousePosition(event) {
mouse.x = event.pageX - canvas.offsetLeft;
mouse.y = event.pageY - canvas.offsetTop;
}
function mouseDown(event) {
if (event.which == 1) {
getMousePosition(event);
mouse.isDown = true;
if (!newBall) newBall = new ball();
newBall.position.x = mouse.x;
newBall.position.y = mouse.y;
}
}
function mouseUp(event) {
if (event.which == 1) {
mouse.isDown = false;
newBall.velocity.y = (newBall.position.y - mouse.y);
newBall.velocity.x = (newBall.position.x - mouse.x);
balls.push(newBall);
newBall = null;
}
}
function setup() {
canvas = document.getElementById("canvas");
width = canvas.getBoundingClientRect().width;
height = canvas.getBoundingClientRect().height;
ctx = canvas.getContext("2d");
canvas.onmousemove = getMousePosition;
canvas.onmousedown = mouseDown;
canvas.onmouseup = mouseUp;
ctx.fillStyle = 'blue';
ctx.strokeStyle = '#000000';
requestAnimationFrame(drawFrame);
frameTimer = setInterval(drawFrame, frameDelay);
tickTimer = setInterval(tick, tickDelay);
}
function tick() {
for (var ball of balls) ball.tick(tickDelay * .001);
}
function drawFrame() {
ctx.clearRect(0, 0, width, height);
for (var ball of balls) ball.show();
if (newBall) newBall.show(ctx);
if (mouse.isDown && newBall) {
ctx.beginPath();
ctx.moveTo(newBall.position.x, newBall.position.y);
ctx.lineTo(mouse.x, mouse.y);
ctx.stroke();
ctx.closePath();
}
}
setup();
#canvas {
border: solid 1px #ccc;
}
<canvas id="canvas"></canvas>
A really simple way would to do exactly the same as you do now, but not initiate all functions as a variable. Change all the variables that are functions to just functions, and where you call them. At least the variable called ball. Then after that you could make two variables like this
ball1 = new ball();
ball2 = new ball();
Your script is kind of messy so hard for me to say if this will go through without any errors, but if it does, I am more than happy to help. This is not the very best solution if you only go for the way i presented now so please do not use this as you solution, but more as a way to get started. Also you will not really learn anything of it if we just gave you the answer
Edit:
Another thing to mark is that using setInterval for games and graphical projects may be a bad idea since JavaScript is single threaded. A better solution is using requestAnimationFrame()
It would look something like this
function mainLoop() {
update();
draw();
requestAnimationFrame(mainLoop);
}
// Start things off
requestAnimationFrame(mainLoop);
I would like to draw a spring in a HTML5 canvas, and show if that spring is at its rest length or not.
My spring is attached to a rectangular shape to some X-Y coordinates and defined as follows:
function Spring(restLenght, width, numRounds){
this.x1 = 0;
this.y1 = 0;
this.x2 = 0;
this.y2 = 0;
this.restLenght = restLenght;
this.width = width;
this.numRounds = numRounds;
this.color = "green";
this.lineWidth = 6;
}
The parameters are explained in the picture below:
When the spring is at its rest length, the lines shall be parallel to each other, otherwise this means the spring is stretched or compressed. Then it will be immediately clear what state the spring is.
I'm stuck now with the bezierCurveTo() Method:
Here is my Fiddle: https://jsfiddle.net/df3mm8kz/1/
var cv = document.getElementById('cv'),
ctx = cv.getContext('2d'),
mouse = capture(cv),
box = new Box(120, 80, 0, 16),
spring = new Spring(160, 20, 2, 0.03, 0.9),
vx = 0,
vy = 0;
function Spring(restLenght, width, numRounds, k, f){
this.x1 = 0;
this.y1 = 0;
this.x2 = 0;
this.y2 = 0;
this.restLenght = restLenght;
this.width = width;
this.numRounds = numRounds;
this.k = k;
this.f = f;
this.color = "green";
this.lineWidth = 6;
}
Spring.prototype.draw = function(ctx) {
var sPX, sPY, cP1X, cP1Y, cP2X, cP2Y, ePX, ePY;
ctx.save();
ctx.translate(this.x, this.y);
ctx.rotate(this.rotation);
ctx.lineWidth = this.lineWidth;
ctx.strokeStyle = this.color;
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.moveTo(this.x1, this.y1);
// length of one spring's round
var l = this.restLenght/(this.numRounds + 2);
// Initial segment, from spring anchor point to the first round
sPX = this.x1+l; sPY = this.y2;
ctx.lineTo(sPX, sPY);
// half width of spring's rounds
var hw = 0.5*this.width;
// half length of one spring's round
var hl = 0.5*l;
for(var i=0, n=this.numRounds; i<n; i++) {
cP1X = sPX + hl*i; cP1Y = sPY + hw;
cP2X = sPX + l*i; cp2Y = sPY + hw;
ePX = sPX + l*i; ePY = sPY;
ctx.bezierCurveTo(cP1X,cP1Y,cP2X,cp2Y,ePX,ePY);
cP1X = sPX + hl*i; cP1Y = sPY - hw;
cP2X = sPX + l*i; cp2Y = sPY - hw;
ePX = sPX + l*i; ePY = sPY;
ctx.bezierCurveTo(cP1X,cP1Y,cP2X,cp2Y,ePX,ePY);
}
// Final segment, from last springs round to the center of mass
ctx.lineTo(this.x2, this.y2);
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.restore();
};
function Box(w, h, mx, my) {
this.x = 0;
this.y = 0;
this.w = w;
this.h = h;
this.mx = mx;
this.my = my;
this.vx = 0;
this.vy = 0;
this.rotation = 0;
this.color = "red";
this.lineWidth = 1;
}
Box.prototype.draw = function(ctx) {
ctx.save();
ctx.translate(this.x, this.y);
ctx.rotate(this.rotation);
ctx.lineWidth = this.lineWidth;
ctx.strokeStyle = "black";
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.rect(-0.5*this.w, -0.5*this.h, this.w, this.h);
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.beginPath();
ctx.strokeStyle = "yellow";
ctx.fillStyle = "yellow";
ctx.arc(this.mx, 0.5*this.h-this.my, 6, 0, 2 * Math.PI, false);
ctx.stroke();
ctx.closePath();
ctx.fill();
ctx.restore();
};
window.requestAnimFrame = (
function(callback) {
return window.setTimeout(callback, 1000/30);
});
(function drawFrame() {
window.requestAnimFrame(drawFrame, cv);
ctx.clearRect(0, 0, cv.width, cv.height);
var dx = box.x - mouse.x,
dy = box.y - mouse.y,
angle = Math.atan2(dy, dx),
boxAngle = angle + 0.5*Math.PI,
targetX = mouse.x + Math.cos(angle) * spring.restLenght,
targetY = mouse.y + Math.sin(angle) * spring.restLenght;
vx += (targetX - box.x) * spring.k;
vy += (targetY - box.y) * spring.k;
vx *= spring.f;
vy *= spring.f;
box.rotation = boxAngle;
box.x += vx;
box.y += vy;
box.draw(ctx);
spring.x1 = mouse.x;
spring.y1 = mouse.y;
spring.x2 = box.x;
spring.y2 = box.y;
spring.draw(ctx);
}());
function capture(element) {
var mouse = {
x: 0,
y: 0,
event: null
},
body_scrollLeft = document.body.scrollLeft,
element_scrollLeft = document.documentElement.scrollLeft,
body_scrollTop = document.body.scrollTop,
element_scrollTop = document.documentElement.scrollTop,
offsetLeft = element.offsetLeft,
offsetTop = element.offsetTop;
element.addEventListener('mousemove', function(event) {
var x, y;
if (event.pageX || event.pageY) {
x = event.pageX;
y = event.pageY;
} else {
x = event.clientX + body_scrollLeft + element_scrollLeft;
y = event.clientY + body_scrollTop + element_scrollTop;
}
x -= offsetLeft;
y -= offsetTop;
mouse.x = x;
mouse.y = y;
mouse.event = event;
}, false);
return mouse;
}
<canvas id="cv" width="600" height="400"></canvas>
Drawing a spring
Rather than use bezier curves which do not actually fit the curve of a spring (but close) I just use a simple path and use trig functions to draw each winding. the function has a start x1,y1 and end x2, y2, windings (should be an integer), width of spring, the offset (bits at ends), Dark colour, and light colour, and the stroke width (width of the wire).
The demo draws an extra highlight to give the spring a little more depth. It can easily be removed.
The code came from this answer that has a simpler version of the same function
function drawSpring(x1, y1, x2, y2, windings, width, offset, col1, col2, lineWidth){
var x = x2 - x1;
var y = y2 - y1;
var dist = Math.sqrt(x * x + y * y);
var nx = x / dist;
var ny = y / dist;
ctx.strokeStyle = col1
ctx.lineWidth = lineWidth;
ctx.lineJoin = "round";
ctx.lineCap = "round";
ctx.beginPath();
ctx.moveTo(x1,y1);
x1 += nx * offset;
y1 += ny * offset;
x2 -= nx * offset;
y2 -= ny * offset;
var x = x2 - x1;
var y = y2 - y1;
var step = 1 / (windings);
for(var i = 0; i <= 1-step; i += step){ // for each winding
for(var j = 0; j < 1; j += 0.05){
var xx = x1 + x * (i + j * step);
var yy = y1 + y * (i + j * step);
xx -= Math.sin(j * Math.PI * 2) * ny * width;
yy += Math.sin(j * Math.PI * 2) * nx * width;
ctx.lineTo(xx,yy);
}
}
ctx.lineTo(x2, y2);
ctx.lineTo(x2 + nx * offset, y2 + ny * offset)
ctx.stroke();
ctx.strokeStyle = col2
ctx.lineWidth = lineWidth - 4;
var step = 1 / (windings);
ctx.beginPath();
ctx.moveTo(x1 - nx * offset, y1 - ny * offset);
ctx.lineTo(x1, y1);
ctx.moveTo(x2, y2);
ctx.lineTo(x2 + nx * offset, y2 + ny * offset)
for(var i = 0; i <= 1-step; i += step){ // for each winding
for(var j = 0.25; j <= 0.76; j += 0.05){
var xx = x1 + x * (i + j * step);
var yy = y1 + y * (i + j * step);
xx -= Math.sin(j * Math.PI * 2) * ny * width;
yy += Math.sin(j * Math.PI * 2) * nx * width;
if(j === 0.25){
ctx.moveTo(xx,yy);
}else{
ctx.lineTo(xx,yy);
}
}
}
ctx.stroke();
}
function display() {
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.clearRect(0, 0, w, h);
ctx.lineWidth = 8;
drawSpring(canvas.width / 2,10, mouse.x,mouse.y,8,100,40,"green","#0C0",15);
}
// Boiler plate code from here down and not part of the answer
var w, h, cw, ch, canvas, ctx, mouse, globalTime = 0, firstRun = true;
;(function(){
const RESIZE_DEBOUNCE_TIME = 100;
var createCanvas, resizeCanvas, setGlobals, resizeCount = 0;
createCanvas = function () {
var c,
cs;
cs = (c = document.createElement("canvas")).style;
cs.position = "absolute";
cs.top = cs.left = "0px";
cs.zIndex = 1000;
document.body.appendChild(c);
return c;
}
resizeCanvas = function () {
if (canvas === undefined) {
canvas = createCanvas();
}
canvas.width = innerWidth;
canvas.height = innerHeight;
ctx = canvas.getContext("2d");
if (typeof setGlobals === "function") {
setGlobals();
}
if (typeof onResize === "function") {
if(firstRun){
onResize();
firstRun = false;
}else{
resizeCount += 1;
setTimeout(debounceResize, RESIZE_DEBOUNCE_TIME);
}
}
}
function debounceResize() {
resizeCount -= 1;
if (resizeCount <= 0) {
onResize();
}
}
setGlobals = function () {
cw = (w = canvas.width) / 2;
ch = (h = canvas.height) / 2;
}
mouse = (function () {
function preventDefault(e) {
e.preventDefault();
}
var mouse = {
x : 0,
y : 0,
w : 0,
alt : false,
shift : false,
ctrl : false,
buttonRaw : 0,
over : false,
bm : [1, 2, 4, 6, 5, 3],
active : false,
bounds : null,
crashRecover : null,
mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",")
};
var m = mouse;
function mouseMove(e) {
var t = e.type;
m.bounds = m.element.getBoundingClientRect();
m.x = e.pageX - m.bounds.left;
m.y = e.pageY - m.bounds.top;
m.alt = e.altKey;
m.shift = e.shiftKey;
m.ctrl = e.ctrlKey;
if (t === "mousedown") {
m.buttonRaw |= m.bm[e.which - 1];
} else if (t === "mouseup") {
m.buttonRaw &= m.bm[e.which + 2];
} else if (t === "mouseout") {
m.buttonRaw = 0;
m.over = false;
} else if (t === "mouseover") {
m.over = true;
} else if (t === "mousewheel") {
m.w = e.wheelDelta;
} else if (t === "DOMMouseScroll") {
m.w = -e.detail;
}
if (m.callbacks) {
m.callbacks.forEach(c => c(e));
}
if ((m.buttonRaw & 2) && m.crashRecover !== null) {
if (typeof m.crashRecover === "function") {
setTimeout(m.crashRecover, 0);
}
}
e.preventDefault();
}
m.addCallback = function (callback) {
if (typeof callback === "function") {
if (m.callbacks === undefined) {
m.callbacks = [callback];
} else {
m.callbacks.push(callback);
}
}
}
m.start = function (element) {
if (m.element !== undefined) {
m.removeMouse();
}
m.element = element === undefined ? document : element;
m.mouseEvents.forEach(n => {
m.element.addEventListener(n, mouseMove);
});
m.element.addEventListener("contextmenu", preventDefault, false);
m.active = true;
}
m.remove = function () {
if (m.element !== undefined) {
m.mouseEvents.forEach(n => {
m.element.removeEventListener(n, mouseMove);
});
m.element.removeEventListener("contextmenu", preventDefault);
m.element = m.callbacks = undefined;
m.active = false;
}
}
return mouse;
})();
// Clean up. Used where the IDE is on the same page.
var done = function () {
window.removeEventListener("resize", resizeCanvas)
mouse.remove();
document.body.removeChild(canvas);
canvas = ctx = mouse = undefined;
}
function update(timer) { // Main update loop
if(ctx === undefined){
return;
}
globalTime = timer;
display(); // call demo code
if (!(mouse.buttonRaw & 2)) {
requestAnimationFrame(update);
} else {
done();
}
}
setTimeout(function(){
resizeCanvas();
mouse.start(canvas, true);
mouse.crashRecover = done;
window.addEventListener("resize", resizeCanvas);
requestAnimationFrame(update);
},0);
})();
/** SimpleFullCanvasMouse.js end **/
To make drawing easier, use .translate() and .rotate() to move into an aligned coordinate system.
ctx.translate(this.x1, this.y1);
ctx.rotate(Math.atan2(this.y2 - this.y1, this.x2 - this.x1));
You can then draw the spring along the local x-axis, and it will appear in the correct place and rotation.
Your spacing of the segments were wrong. hl*i is half the distance from the spring's starting point, not the segment's starting point.
var cv = document.getElementById('cv'),
ctx = cv.getContext('2d'),
mouse = capture(cv),
box = new Box(120, 80, 0, 16),
spring = new Spring(160, 50, 2, 0.03, 0.9),
vx = 0,
vy = 0;
function Spring(restLenght, width, numRounds, k, f) {
this.x1 = 0;
this.y1 = 0;
this.x2 = 0;
this.y2 = 0;
this.restLenght = restLenght;
this.width = width;
this.numRounds = numRounds;
this.k = k;
this.f = f;
this.color = "green";
this.lineWidth = 6;
}
Spring.prototype.draw = function(ctx) {
var sPX, sPY, cP1X, cP1Y, cP2X, cP2Y, ePX, ePY;
ctx.save();
ctx.lineWidth = this.lineWidth;
ctx.strokeStyle = this.color;
ctx.fillStyle = this.color;
var vx = this.x2 - this.x1;
var vy = this.y2 - this.y1;
var vm = Math.sqrt(vx * vx + vy * vy);
ctx.translate(this.x1, this.y1);
ctx.rotate(Math.atan2(vy, vx));
ctx.beginPath();
ctx.moveTo(0, 0);
// length of one spring's round
var l = vm / (this.numRounds + 2);
// Initial segment, from spring anchor point to the first round
sPX = l;
sPY = 0;
ctx.lineTo(sPX, sPY);
// half width of spring's rounds
var hw = 0.5 * this.width;
for (var i = 0, n = this.numRounds; i < n; i++) {
cP1X = sPX + l * (i + 0.0);
cP1Y = sPY + hw;
cP2X = sPX + l * (i + 0.5);
cp2Y = sPY + hw;
ePX = sPX + l * (i + 0.5);
ePY = sPY;
ctx.bezierCurveTo(cP1X, cP1Y, cP2X, cp2Y, ePX, ePY);
cP1X = sPX + l * (i + 0.5);
cP1Y = sPY - hw;
cP2X = sPX + l * (i + 1.0);
cp2Y = sPY - hw;
ePX = sPX + l * (i + 1.0);
ePY = sPY;
ctx.bezierCurveTo(cP1X, cP1Y, cP2X, cp2Y, ePX, ePY);
}
// Final segment, from last springs round to the center of mass
ctx.lineTo(vm, 0);
//ctx.closePath();
//ctx.fill();
ctx.stroke();
ctx.restore();
};
function Box(w, h, mx, my) {
this.x = 0;
this.y = 0;
this.w = w;
this.h = h;
this.mx = mx;
this.my = my;
this.vx = 0;
this.vy = 0;
this.rotation = 0;
this.color = "red";
this.lineWidth = 1;
}
Box.prototype.draw = function(ctx) {
ctx.save();
ctx.translate(this.x, this.y);
ctx.rotate(this.rotation);
ctx.lineWidth = this.lineWidth;
ctx.strokeStyle = "black";
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.rect(-0.5 * this.w, -0.5 * this.h, this.w, this.h);
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.beginPath();
ctx.strokeStyle = "yellow";
ctx.fillStyle = "yellow";
ctx.arc(this.mx, 0.5 * this.h - this.my, 6, 0, 2 * Math.PI, false);
ctx.stroke();
ctx.closePath();
ctx.fill();
ctx.restore();
};
window.requestAnimFrame = (
function(callback) {
return window.setTimeout(callback, 1000 / 30);
});
(function drawFrame() {
window.requestAnimFrame(drawFrame, cv);
ctx.clearRect(0, 0, cv.width, cv.height);
var dx = box.x - mouse.x,
dy = box.y - mouse.y,
angle = Math.atan2(dy, dx),
boxAngle = angle + 0.5 * Math.PI,
targetX = mouse.x + Math.cos(angle) * spring.restLenght,
targetY = mouse.y + Math.sin(angle) * spring.restLenght;
vx += (targetX - box.x) * spring.k;
vy += (targetY - box.y) * spring.k;
vx *= spring.f;
vy *= spring.f;
box.rotation = boxAngle;
box.x += vx;
box.y += vy;
box.draw(ctx);
spring.x1 = mouse.x;
spring.y1 = mouse.y;
spring.x2 = box.x;
spring.y2 = box.y;
spring.draw(ctx);
}());
function capture(element) {
var mouse = {
x: 0,
y: 0,
event: null
},
body_scrollLeft = document.body.scrollLeft,
element_scrollLeft = document.documentElement.scrollLeft,
body_scrollTop = document.body.scrollTop,
element_scrollTop = document.documentElement.scrollTop,
offsetLeft = element.offsetLeft,
offsetTop = element.offsetTop;
element.addEventListener('mousemove', function(event) {
var x, y;
if (event.pageX || event.pageY) {
x = event.pageX;
y = event.pageY;
} else {
x = event.clientX + body_scrollLeft + element_scrollLeft;
y = event.clientY + body_scrollTop + element_scrollTop;
}
x -= offsetLeft;
y -= offsetTop;
mouse.x = x;
mouse.y = y;
mouse.event = event;
}, false);
return mouse;
}
<canvas id="cv" width="600" height="400"></canvas>
How to move the square to the destination? Square moves one pixel only when click the mouse? Sorry for my english.
window.onload = function(){
var x = 50;
var y = 50;
var c = document.getElementById("game");
var ctx = c.getContext("2d");
init();
draw();
function init()
{
document.addEventListener("click",paint,false);
}
function paint(e)
{
if(x<e.clientX) x++;
}
function draw()
{
ctx.clearRect(x-1,y,1,15);
ctx.fillStyle = "blue";
ctx.fillRect(x,y,15,15);
window.requestAnimationFrame(draw);
}
}
Here is one way to do it, adapted from this article that I wrote a few months back.
The following is the piece to get it working
var tx = targetX - x,
ty = targetY - y,
dist = Math.sqrt(tx*tx+ty*ty);
velX = (tx/dist)*thrust;
velY = (ty/dist)*thrust;
We need to get the difference between the current position and targeted position (clicked area), we then get the distance, and make the velocity for x and y equal to the difference divided by the total distance multiplied by the speed of the object.
Full working example and code
Live demo
var canvas = document.getElementById("canvas"),
ctx = canvas.getContext("2d"),
width = 500,
height = 500,
mX = width/2,
mY = height/2;
canvas.width = width;
canvas.height = height;
canvas.addEventListener("click", function (e) {
mX = e.pageX;
mY = e.pageY;
});
var Ball = function (x, y, radius, color) {
this.x = x || 0;
this.y = y || 0;
this.radius = radius || 10;
this.speed = 5;
this.color = color || "rgb(255,0,0)";
this.velX = 0;
this.velY = 0;
}
Ball.prototype.update = function (x, y) {
// get the target x and y
this.targetX = x;
this.targetY = y;
// We need to get the distance this time around
var tx = this.targetX - this.x,
ty = this.targetY - this.y,
dist = Math.sqrt(tx * tx + ty * ty);
/*
* we calculate a velocity for our object this time around
* divide the target x and y by the distance and multiply it by our speed
* this gives us a constant movement speed.
*/
this.velX = (tx / dist) * this.speed;
this.velY = (ty / dist) * this.speed;
// Stop once we hit our target. This stops the jittery bouncing of the object.
if (dist > this.radius / 2) {
// add our velocities
this.x += this.velX;
this.y += this.velY;
}
};
Ball.prototype.render = function () {
ctx.fillStyle = this.color;
ctx.beginPath();
// draw our circle with x and y being the center
ctx.arc(this.x - this.radius / 2, this.y - this.radius / 2, this.radius, 0, Math.PI * 2);
ctx.closePath();
ctx.fill();
};
var ball1 = new Ball(width / 2, height / 2, 10);
function render() {
ctx.clearRect(0, 0, width, height);
ball1.update(mX, mY);
ball1.render();
requestAnimationFrame(render);
}
render();