Pan around the canvas within bounds smoothly - javascript

Most other solutions I have found relate to images as the bounds and work through css, and I can't get them to work with a canvas.
I want to create the bounds by defining an arbitrary percent size of the canvas (e.g 2x - twice the size) that expands from the center of the canvas and then have the canvas view (red) move/pan around within the bounds (green) by mouse hovering inside the canvas - that it moves to all corners of the bounds from the center of the canvas.
like this behavior in terms of the direction with panning
http://jsfiddle.net/georgedyer/XWnUA/2/
Additionally, I would like
the solution to work with a responsive canvas sizes rather than fixed canvas sizes
the panning to be smooth using easing such as CubicInOut easing
to detect the mouse hover inside the circle drawn. Might have to change the coordinate from screen to world to get it to work properly. But maybe theirs an easier way?
formulas
Here's my attempt at figuring out the formulas to get it working! I'll just focus on just one axis (x-axis) from left to right of the mouse, but the same formulas should apply to the y-axis.
First I get the "percentX" of the mouse along the width. If the "percentX" is 0 then the "newX" for the pan will be the value of the leftSide, else if it's 1 then it'll be the rightSide of the boundsBorder (green). So the "percentX" determines which side of the boundsBorder/limit the canvas view will extend/move to.
Only problem is that this doesn't work. So my formula must be wrong. The mouse inside the circle event is also inaccurate because of my canvas translate. Combining everything together is difficult to figure out!
Code
<html>
<head>
<meta charset="UTF-8">
<title>Panning</title>
<style type="text/css">
* {
margin: 0;
padding: 0;
}
body {
background: #eee;
padding: 20px 0;
}
.canvas-container {
height: 400px;
width: 100%;
margin-bottom: 40px;
border-radius: 10px;
background: #fff;
overflow: hidden;
}
canvas {
width: 100%;
height: inherit;
}
.container {
width: 60%;
margin: 0 auto;
}
</style>
</head>
<body>
<!-- CANVAS -->
<div class="container">
<div class="canvas-container">
<canvas id="canvas"></canvas>
</div>
</div>
<!-- SCRIPT -->
<script type="text/javascript">
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var width = canvas.width = canvas.clientWidth;
var height = canvas.height = canvas.clientHeight;
var panX = 0, panY = 0;
var scaleBorderFactor = 2; /* scale factor for the bounds */
var mouse = {
x: 0,
y: 0
}
function Circle(x, y, radius, color){
this.x = x;
this.y = y;
this.radius = radius;
this.color = color;
this.draw = function(ctx){
ctx.beginPath();
ctx.arc(this.x,this.y,this.radius,0,Math.PI*2,false);
ctx.fillStyle = this.color;
ctx.fill();
ctx.closePath();
}
}
/* Rect Stroke Borders for visual reference */
function RectBorder(x, y, w, h, c){
this.x = x;
this.y = y;
this.width = w;
this.height = h;
this.color = c;
this.draw = function(ctx){
ctx.strokeStyle = this.color;
ctx.lineWidth = 2;
ctx.strokeRect(this.x, this.y, this.width, this.height);
};
/* Draw rect from a center point */
this.drawAtCenter = function(ctx){
ctx.strokeStyle = this.color;
ctx.lineWidth = 2;
ctx.strokeRect(this.x, this.y, this.width, this.height);
};
this.toString = function(){
console.log("x: "+this.x+", y: "+this.y+", w: "+this.width+", h: "+this.height+", color = "+this.color);
}
}
function getMousePos(canvas, event) {
var rect = canvas.getBoundingClientRect();
return {
x: (event.clientX - rect.left),
y: (event.clientY - rect.top)
};
}
function insideCircle(circle, mouse){
var dist = Math.sqrt(Math.pow(circle.x - mouse.x,2) + Math.pow(circle.y - mouse.y,2));
console.log(circle.radius+", d = "+dist)
return dist <= circle.radius;
}
function lerp(start, end, percent){
return start*(1 - percent) + percent*end;
}
/* t: current time, b: beginning value, c: change in value, d: duration */
function easeInOutCubic(t, b, c, d) {
if ((t/=d/2) < 1) return c/2*t*t*t + b;
return c/2*((t-=2)*t*t + 2) + b;
}
var canvasBorder = new RectBorder(0,0,canvas.width,canvas.height,'red');
var boundsBorder = new RectBorder(-(canvas.width*scaleBorderFactor - canvas.width)/2,-(canvas.height*scaleBorderFactor - canvas.height)/2,canvas.width*scaleBorderFactor,canvas.height*scaleBorderFactor,'green');
var circle = new Circle(200,200,40,'blue');
/* Draw Update */
update();
function update(){
canvas = document.getElementById("canvas");
ctx = canvas.getContext("2d");
width = canvas.width = canvas.clientWidth;
height = canvas.height = canvas.clientHeight;
ctx.clearRect(0,0,width,height);
ctx.save();
/* MOUSE */
percentX = (mouse.x / width);
percentY = (mouse.y / height);
/* the 2 sides of boundsBorder */
var leftSide = (width*scaleBorderFactor - width)/2 - width;
var rightSide = (width*scaleBorderFactor - width)/2 + width;
var topSide = (height*scaleBorderFactor - height)/2 - width;
var bottomSide = (height*scaleBorderFactor - height)/2 + height;
newX = rightSide * percentX + leftSide * (1 - percentX);
newY = bottomSide * percentY + topSide * (1 - percentY);
/* maybe use easeInOutCubic for better smoothness */
panX = lerp(-newX, mouse.x, 0.1);
panY = lerp(-newY, mouse.y, 0.1);
if (insideCircle(circle, mouse)){
circle.color = "pink";
} else {
circle.color = "blue";
}
ctx.translate(panX,panY);
/* Draw both borders only for reference */
canvasBorder.draw(ctx);
boundsBorder.drawAtCenter(ctx);
/* Draw Circle */
circle.draw(ctx);
ctx.restore();
requestAnimationFrame(update);
}
/* Events */
function mousemove(e) {
mouse = getMousePos(canvas, e);
}
/* Event Listeners */
canvas.addEventListener('mousemove', mousemove);
</script>
</body>
</html>
EDIT
I have managed to get the pan functionality to work. I just realized that all I need to do is offset by the positive and negative "widthGap"(see picture below) amount when panning. So it goes from positive "widthGap" (moves to the rght) to negative "widthGap" (moves to the left) when the "percentX" changes value like mentioned previously.
To get the smooth movement, I instead applied the lerp to the "percentX" values.
The code below is what should be replaced from the previous code above. It also shows the new variables I defined, which will make some of the variables from the previous code redundant.
UPDATED Code
var canvasBorder = new RectBorder(0,0,canvas.width,canvas.height,'red');
/* widthGap is the new leftSide formula.
* it is the gap differance between border and canvas
*/
var widthGap = (canvas.width*scaleBorderFactor - canvas.width)/2;
var heightGap = (canvas.height*scaleBorderFactor - canvas.height)/2;
var boundsBorder = new RectBorder(-widthGap,-heightGap,canvas.width+widthGap*2,canvas.height+heightGap*2,'green');
var circle = new Circle(200,200,40,'blue');
/* Draw Update */
update();
var newPercentX = 0, newPercentY = 0;
var percentX = 0, percentY = 0;
function update(){
canvas = document.getElementById("canvas");
ctx = canvas.getContext("2d");
width = canvas.width = canvas.clientWidth;
height = canvas.height = canvas.clientHeight;
ctx.clearRect(0,0,width,height);
ctx.save();
newPercentX = (mouse.x / width);
newPercentY = (mouse.y / height);
/* MOUSE */
percentX = lerp(percentX,newPercentX,0.05);
percentY = lerp(percentY,newPercentY,0.05);
panX = (widthGap) * percentX + (-widthGap) * (1 - percentX);
panY = (heightGap) * percentY + (-heightGap) * (1 - percentY);
ctx.translate(-panX,-panY);
if (insideCircle(circle, mouse)){
circle.color = "pink";
} else {
circle.color = "blue";
}
/* Draw both borders only for reference */
canvasBorder.draw(ctx);
boundsBorder.drawAtCenter(ctx);
/* Draw Circle */
circle.draw(ctx);
ctx.restore();
requestAnimationFrame(update);
}

Related

Determine if mouse was clicked on a rotated rectangle on a canvas

I am using the example that has been posted in the following link:
https://riptutorial.com/html5-canvas/example/19666/a-transformation-matrix-to-track-translated--rotated---scaled-shape-s-
I am trying to use this example, with some modifications to a allow for rotating 2 rectangles on the canvas. There are several issues (and they may be related), but let me just start with one:
Mouse click no longer works.
I hope someone can help point to me to what is the needed remedy as I struggled with this for a over a day and obviously I am not savvy with Objects, Classes in Javascrip. The code is here: https://jsfiddle.net/jackmstein/ngyfrcms/2/
<!doctype html>
<html>
<head>
<style>
body{ background-color:white; }
#canvas{border:1px solid red; }
</style>
<script>
window.onload=(function(){
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
function reOffset(){
var BB=canvas.getBoundingClientRect();
offsetX=BB.left;
offsetY=BB.top;
}
var offsetX,offsetY;
reOffset();
window.onscroll=function(e){ reOffset(); }
window.onresize=function(e){ reOffset(); }
// Transformation Matrix "Class"
var TransformationMatrix=( function(){
// private
var self;
var m=[1,0,0,1,0,0];
var reset=function(){ var m=[1,0,0,1,0,0]; }
var multiply=function(mat){
var m0=m[0]*mat[0]+m[2]*mat[1];
var m1=m[1]*mat[0]+m[3]*mat[1];
var m2=m[0]*mat[2]+m[2]*mat[3];
var m3=m[1]*mat[2]+m[3]*mat[3];
var m4=m[0]*mat[4]+m[2]*mat[5]+m[4];
var m5=m[1]*mat[4]+m[3]*mat[5]+m[5];
m=[m0,m1,m2,m3,m4,m5];
}
var screenPoint=function(transformedX,transformedY){
// invert
var d =1/(m[0]*m[3]-m[1]*m[2]);
im=[ m[3]*d, -m[1]*d, -m[2]*d, m[0]*d, d*(m[2]*m[5]-m[3]*m[4]), d*(m[1]*m[4]-m[0]*m[5]) ];
// point
return({
x:transformedX*im[0]+transformedY*im[2]+im[4],
y:transformedX*im[1]+transformedY*im[3]+im[5]
});
}
var transformedPoint=function(screenX,screenY){
return({
x:screenX*m[0] + screenY*m[2] + m[4],
y:screenX*m[1] + screenY*m[3] + m[5]
});
}
// public
function TransformationMatrix(){
self=this;
}
// shared methods
TransformationMatrix.prototype.translate=function(x,y){
var mat=[ 1, 0, 0, 1, x, y ];
multiply(mat);
};
TransformationMatrix.prototype.rotate=function(rAngle){
var c = Math.cos(rAngle);
var s = Math.sin(rAngle);
var mat=[ c, s, -s, c, 0, 0 ];
multiply(mat);
};
TransformationMatrix.prototype.scale=function(x,y){
var mat=[ x, 0, 0, y, 0, 0 ];
multiply(mat);
};
TransformationMatrix.prototype.skew=function(radianX,radianY){
var mat=[ 1, Math.tan(radianY), Math.tan(radianX), 1, 0, 0 ];
multiply(mat);
};
TransformationMatrix.prototype.reset=function(){
reset();
}
TransformationMatrix.prototype.setContextTransform=function(ctx){
ctx.setTransform(m[0],m[1],m[2],m[3],m[4],m[5]);
}
TransformationMatrix.prototype.resetContextTransform=function(ctx){
ctx.setTransform(1,0,0,1,0,0);
}
TransformationMatrix.prototype.getTransformedPoint=function(screenX,screenY){
return(transformedPoint(screenX,screenY));
}
TransformationMatrix.prototype.getScreenPoint=function(transformedX,transformedY){
return(screenPoint(transformedX,transformedY));
}
TransformationMatrix.prototype.getMatrix=function(){
var clone=[m[0],m[1],m[2],m[3],m[4],m[5]];
return(clone);
}
// return public
return(TransformationMatrix);
})();
// DEMO starts here
// create a rect and add a transformation matrix
// to track it's translations, rotations & scalings
var rect1={x:30,y:30,w:50,h:35,matrix:new TransformationMatrix()};
// draw the untransformed rect in black
// ctx.strokeRect(rect.x, rect.y, rect.w, rect.h);
// Demo: label
// ctx.font='11px arial';
// ctx.fillText('Untransformed Rect',rect.x,rect.y-10);
// transform the canvas & draw the transformed rect in red
ctx.translate(100,0);
ctx.scale(2,2);
ctx.rotate(Math.PI/12);
// draw the transformed rect
ctx.strokeStyle='red';
ctx.strokeRect(rect1.x, rect1.y, rect1.w, rect1.h);
ctx.font='6px arial';
// Demo: label
ctx.fillText('Rect: Translated, rotated & scaled',rect1.x,rect1.y-6);
// reset the context to untransformed state
ctx.setTransform(1,0,0,1,0,0);
// record the transformations in the matrix
var m=rect1.matrix;
m.translate(100,0);
m.scale(2,2);
m.rotate(Math.PI/12);
// use the rect's saved transformation matrix to reposition,
// resize & redraw the rect
ctx.strokeStyle='blue';
drawTransformedRect(rect1);
// Demo: instructions
ctx.font='14px arial';
ctx.fillText('Demo: click inside the blue rect',30,200);
// DEMO starts here
// create a rect and add a transformation matrix
// to track it's translations, rotations & scalings
var rect2={x:150,y:30,w:50,h:35,matrix:new TransformationMatrix()};
// draw the untransformed rect in black
// ctx.strokeRect(rect.x, rect.y, rect.w, rect.h);
// Demo: label
// ctx.font='11px arial';
// ctx.fillText('Untransformed Rect',rect.x,rect.y-10);
// transform the canvas & draw the transformed rect in red
ctx.translate(100,0);
ctx.scale(2,2);
ctx.rotate(Math.PI/12);
// draw the transformed rect
ctx.strokeStyle='red';
ctx.strokeRect(rect2.x, rect2.y, rect2.w, rect2.h);
ctx.font='6px arial';
// Demo: label
ctx.fillText('Rect: Translated, rotated & scaled',rect2.x,rect2.y-6);
// reset the context to untransformed state
ctx.setTransform(1,0,0,1,0,0);
// record the transformations in the matrix
var m=rect2.matrix;
m.translate(100,0);
m.scale(2,2);
m.rotate(Math.PI/12);
// use the rect's saved transformation matrix to reposition,
// resize & redraw the rect
ctx.strokeStyle='blue';
drawTransformedRect(rect2);
// Demo: instructions
// redraw a rect based on it's saved transformation matrix
function drawTransformedRect(r){
// set the context transformation matrix using the rect's saved matrix
m.setContextTransform(ctx);
// draw the rect (no position or size changes needed!)
ctx.rect( r.x, r.y, r.w, r.h );
// reset the context transformation to default (==untransformed);
m.resetContextTransform(ctx);
}
// is the point in the transformed rectangle?
function isPointInTransformedRect(r,transformedX,transformedY){
var p=r.matrix.getScreenPoint(transformedX,transformedY);
var x=p.x;
var y=p.y;
return(x>r.x && x<r.x+r.w && y>r.y && y<r.y+r.h);
}
// listen for mousedown events
canvas.onmousedown=handleMouseDown;
function handleMouseDown(e){
// tell the browser we're handling this event
e.preventDefault();
e.stopPropagation();
// get mouse position
mouseX=parseInt(e.clientX-offsetX);
mouseY=parseInt(e.clientY-offsetY);
// is the mouse inside the transformed rect?
var rect1={x:30,y:30,w:50,h:35,matrix:new TransformationMatrix()};
if(isPointInTransformedRect(rect1,mouseX,mouseY)){
alert('You clicked in the transformed Rect 1');
}
var rect2={x:150,y:30,w:50,h:35,matrix:new TransformationMatrix()};
if(isPointInTransformedRect(rect2,mouseX,mouseY)){
alert('You clicked in the transformed Rect 2');
}
}
// Demo: redraw transformed rect without using
// context transformation commands
function drawTransformedRect(r,color){
var m=r.matrix;
var tl=m.getTransformedPoint(r.x,r.y);
var tr=m.getTransformedPoint(r.x+r.w,r.y);
var br=m.getTransformedPoint(r.x+r.w,r.y+r.h);
var bl=m.getTransformedPoint(r.x,r.y+r.h);
ctx.beginPath();
ctx.moveTo(tl.x,tl.y);
ctx.lineTo(tr.x,tr.y);
ctx.lineTo(br.x,br.y);
ctx.lineTo(bl.x,bl.y);
ctx.closePath();
ctx.strokeStyle=color;
ctx.stroke();
}
}); // end window.onload
</script>
</head>
<body>
<canvas id="canvas" width=600 height=250></canvas>
</body>
</html>
Point over rotated rectangle
NOTE this answer uses a uniform transform. (Skew will not work, and both x and y axis must have the same scale);
To keep it all as simple as possible the rectangle (box) is defined by its center pos and its width and height size.
const Point = (x = 0, y = 0) => ({x, y});
const Size = (w = 0, h = 0) => ({w, h});
const Rect = (pos, size) = ({pos, size});
We can create the rotated (transformed) rect by creating the transform as needed. (quicker than creating a DOMMatrix or custom matrix)
function pathRect(rect, ang, scale) { // ang in radians
const xax = Math.cos(ang) * scale;
const xay = Math.sin(ang) * scale;
ctx.setTransform(xax, xay, -xay, xax, rect.pos.x, rect.pos.y);
ctx.rect(-rect.size.w * 0.5, -rect.size.h * 0.5, rect.size.w, rect.size.h);
}
To find the point relative to the rect we apply the inverse (reverse) of the transform applied to the rect.
relPoint is converted to a box relative unit value. That is, if the point is at the top left (of rotated rect) relPoint will be {x:0, y:0}, at center {x: 0.5, y: 0.5} and bottom right is {x: 1, y: 1}. This simplifies the bounds test (see demo)
function pointRelative2Rect(point, relPoint, rect, ang, scale) {
const xax = Math.cos(-ang) / scale;
const xay = Math.sin(-ang) / scale;
const x = point.x - rect.pos.x;
const y = point.y - rect.pos.y;
relPoint.x = (xax * x - xay * y) / rect.size.w + 0.5;
relPoint.y = (xay * x + xax * y) / rect.size.h + 0.5;
}
Thus if we have a mouse pos then we can find when its over the rect using
pointRelative2Rect(mouse, mouseRel, rect, rot, scale);
if (mouseRel.x < 0 || mouseRel.x > 1 || mouseRel.y < 0 || mouseRel.y > 1) {
// mouse outside rect
} else {
// mouse over rect
}
Demo
Demo creates a random rectangle that is animated to rotate and scale over time.
It uses the methods outlined above to test if the mouse is inside the rect.
The box will change to red when mouse is over.
const ctx = canvas.getContext("2d");
const w = 256, h = 256;
const Point = (x = 0, y = 0) => ({x, y});
const Size = (w = 0, h = 0) => ({w, h});
const Rect = (pos, size) => ({pos, size});
const randI = (min, max) => Math.random() * (max - min) + min;
const mouse = Point(), mouseRel = Point();
document.addEventListener("mousemove", e => { mouse.x = e.pageX; mouse.y = e.pageY; });
const randRect = () => Rect(
Point(randI(40, 210), randI(40, 210)),
Size(randI(10, 30), randI(10, 30))
);
const rect = randRect();
function pathRect(rect, ang, scale) { // ang in radians
const xax = Math.cos(ang) * scale;
const xay = Math.sin(ang) * scale;
ctx.setTransform(xax, xay, -xay, xax, rect.pos.x, rect.pos.y);
ctx.rect(-rect.size.w * 0.5, -rect.size.h * 0.5, rect.size.w, rect.size.h);
}
function pointRelative2Rect(point, resPoint, rect, ang, scale) {
const xax = Math.cos(-ang) / scale;
const xay = Math.sin(-ang) / scale;
const x = point.x - rect.pos.x;
const y = point.y - rect.pos.y;
resPoint.x = (xax * x - xay * y) / rect.size.w + 0.5;
resPoint.y = (xay * x + xax * y) / rect.size.h + 0.5;
}
requestAnimationFrame(renderLoop);
function renderLoop(time) {
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0, 0, w, h);
const rot = time / 3000;
const scale = Math.sin(time / 2777) * 0.5 + 2.1;
ctx.beginPath();
pointRelative2Rect(mouse, mouseRel, rect, rot, scale);
if (mouseRel.x < 0 || mouseRel.x > 1 || mouseRel.y < 0 || mouseRel.y > 1) {
canvas.style.cursor = "default";
ctx.strokeStyle = "#000";
} else {
canvas.style.cursor = "pointer";
ctx.strokeStyle = "#F00";
}
pathRect(rect, rot, scale);
ctx.lineWidth = 1 / scale;
ctx.stroke();
requestAnimationFrame(renderLoop);
}
canvas {
position : absolute;
top : 0px;
left : 0px;
}
<canvas id="canvas" width="256" height="256"><canvas>

how to draw lines simultaneously show x & y coordinators on top of mouse pointer using canvas and javascript

draw lines while mouseDown and simultaneously show x & y coordinators on top of mouse pointer while MouseMove using canvas and javascript.
Here x & y coordinators are continuously drawing on top of mouse pointer while MouseMove. Then I am unable Draw Lines while MouseDown since i am using ctxTemp.clearRect(0,0,canvasTemp.width,canvasTemp.height);
if i am not using ctxTemp.clearRect(0,0,canvasTemp.width,canvasTemp.height); then x & y coordinators are continuously drawing on top of mouse pointer while MouseMove.
thanks is advance.
Double buffer
It is a common task to render addition guides (coordinates, widgets, etc...) on the canvas while creating content.
Using a single canvas this become problematic as you are overwriting the content by clearing, or the just painting the guides .
The solution is to use an additional (or more) canvas to separate the content from the guides.
Example
The example shows how this is done.
A second canvas is created call drawing. It matches the size of the canvas on the page.
The mouse draws the stroke to the second canvas.
The main update function draws the second canvas onto the main canvas and then draws the mouse position in a box over that.
As you can not draw outside the canvas some additional code is needed when drawing the mouse position to prevent it from going outside the canvas.
requestAnimationFrame(update);
const ctx = canvas.getContext("2d");
var w = canvas.width, h = canvas.height;
const drawing = createImage(w, h); // create canvas to hold drawing
const pointQueue = []; // holds points when mouse button down
drawing.ctx.lineWidth = 4;
drawing.ctx.strokeStyle = "#F00";
drawing.ctx.lineJoin = "round";
drawing.ctx.lineCap = "round";
ctx.font = "16px Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
/* add mouse listeners */
const bounds = canvas.getBoundingClientRect();
const mouse = {x: 0, y: 0, button: false}, events = ["down", "up", "move"];
events.forEach(name => document.addEventListener("mouse" + name, mouseEvents));
function drawMousePos(ctx) {
const text = "X: " + mouse.x.toFixed(0) + " Y: " + mouse.y.toFixed(0);
const width = ctx.measureText(text).width + 8;
var x = mouse.x, y = mouse.y - 18;
if (x + width / 2 > w) { x = w - width / 2 }
if (x - width / 2 < 0) { x = width / 2 }
if (y - 10 < 0) { y = 10 }
if (y + 10 > h) { y = h - 10 }
ctx.fillStyle = "#EEC8";
ctx.fillRect(x - width / 2, y - 12, width , 20);
ctx.strokeRect(x - width / 2, y - 12, width, 20);
ctx.fillStyle = "#000C";
ctx.fillText(text, x, y);
}
function drawPen(ctx) {
if (pointQueue.length >= 2) {
ctx.beginPath();
ctx.moveTo(...pointQueue.shift());
while (pointQueue.length > (mouse.button ? 1 : 0)) { ctx.lineTo(...pointQueue.shift()) }
pointQueue.length && ctx.lineTo(...pointQueue[0]);
ctx.stroke();
}
}
function update(){
if (pointQueue.length) {
drawPen(drawing.ctx);
ctx.clearRect(0, 0, w, h);
ctx.drawImage(drawing, 0, 0);
pointQueue.length && drawMousePos(ctx);
canvas.style.cursor = "none";
} else { canvas.style.cursor = "crosshair" }
requestAnimationFrame(update);
}
function createImage(w, h){
const can = document.createElement("canvas");
can.width = w;
can.height = h;
can.ctx = can.getContext("2d");
return can;
}
function mouseEvents(e){
mouse.x = e.pageX - bounds.left - 2; // offset by 2 pixels for canvas border
mouse.y = e.pageY - bounds.top - 2;
if (e.type === "mousedown") { mouse.button = true }
if (mouse.button) { pointQueue.push([mouse.x , mouse.y]) }
if (e.type === "mouseup") { mouse.button = false }
}
canvas {
border : 2px solid black;
cursor: crosshair;
}
Click drag mouse to draw<br>
<canvas id="canvas" width="512" height="256"></canvas>
you could try binding a div or a span to your mouse and attach it an eventListener like 'mousemove', in this div you could have two spans like
<div id='mouseCoord" style="position: absolute; top:0,left:0">
<span id='mouseX'><span>
<span id='mouseY'><span>
</div>
The div #mouseCoord would have an absolute position, and on mouse move, you could update his top/left position
const mouseDiv = document.getElementById('mouseCoord')
const mouseX = document.getElementById('mouseX')
const mouseY = document.getElementById('mouseY')
mouseDiv.addEventListener('mousemove', e => {
mouseDiv.setAttribute('style', `left: ${e.offsetX};top: ${e.offsetY}`)
mouseY.innerText = `y: ${e.offsetY}`
mouseY.innerText = `x: ${e.offsetX}`
})
With this in place, you could add some mouse events to control when you want to display the coordinate like only when the mouse is moving etc ...
I hope this can helps

How to change the center of background's rotation on my canvas?

Im working on a project that will be like a game (like gta v) minimap, but to track the user's movement over a field. The field is a 2K image, and the user is represented by a black triangle. But the triangle is positionated on the bottom of the field, not the center. And to rotate the background (as the user turns) I'm using the code from this answer's post: Bryan Field's answer, it works perfect, but the anchor's rotation is on center, I wanna this to be on the triangle, but I can't figure it out the maths to do it (in a way to not break the background image bounds). The triangle position is (canvas.width/2, canvas.height-100) and the rotation's anchor is in (canvas.width/2, canvas.height/2). Here's a screenshoot:
const ctx = document.querySelector('canvas').getContext('2d');
const mapImage = new Image();
mapImage.onload = start;
mapImage.src = 'https://upload.wikimedia.org/wikipedia/commons/thumb/a/ab/Map_of_the_Battle_of_the_Somme%2C_1916.svg/1568px-Map_of_the_Battle_of_the_Somme%2C_1916.svg.png';
// wedge pointing left
const playerPath = new Path2D();
playerPath.lineTo(15, 0);
playerPath.lineTo(-15, 10);
playerPath.lineTo(-15, -10);
playerPath.closePath();
function start() {
const keys = {};
const player = {
x: mapImage.width / 2,
y: mapImage.height / 2,
turnVel: Math.PI / 2, // 1/4 turn per second
dir: -Math.PI / 2,
vel: 10, // 10 units per second
};
let then = 0;
function render(now) {
now *= 0.001; // convert to seconds
const deltaTime = now - then;
then = now;
resizeCanvasToDisplaySize(ctx.canvas);
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
let turnDir = 0;
if (keys[37]) {
turnDir = -1;
} else if (keys[39]) {
turnDir = 1;
}
player.dir += player.turnVel * turnDir * deltaTime;
player.x += Math.cos(player.dir) * player.vel * deltaTime;
player.y += Math.sin(player.dir) * player.vel * deltaTime;
ctx.save();
{
// move origin to bottom center of canvas
ctx.translate(ctx.canvas.width / 2, ctx.canvas.height * 0.9);
ctx.save();
{
// rotate origin opposite of player
// the -Math.PI/2 is because the code above adding the direction
// to the velocity has dir = 0 meaning going east.
// We want to face up the map. Up is -Math.PI/2
// so when going up we need the rotation here to be 0. (not rotated)
ctx.rotate(-player.dir - Math.PI/ 2);
// move origin based on player's position
ctx.translate(-player.x, -player.y);
ctx.drawImage(mapImage, 0, 0);
// draw enemies or targets here. They are in map coordinates
// example
ctx.fillStyle = 'red';
for (let y = 0; y < mapImage.height; y += 200) {
for (let x = 0; x < mapImage.width; x += 200) {
ctx.save();
{
ctx.translate(x, y);
ctx.rotate(Math.atan2(player.y - y, player.x - x));
ctx.fill(playerPath);
}
ctx.restore();
}
}
}
ctx.restore(); // origin back at bottom center of canvas
// draw Player
ctx.save();
{
ctx.rotate(-Math.PI / 2); // because the player's shape points left
ctx.fillStyle = 'black';
ctx.fill(playerPath);
}
ctx.restore();
}
ctx.restore(); // origin back at top left corner of canvas
requestAnimationFrame(render);
}
requestAnimationFrame(render);
window.addEventListener('keydown', (e) => {
keys[e.keyCode] = true;
});
window.addEventListener('keyup', (e) => {
keys[e.keyCode] = false;
});
function resizeCanvasToDisplaySize(canvas) {
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
canvas.width = width;
canvas.height = height;
}
return needResize;
}
}
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
#info {
position: absolute;
left: 1em;
top: 1em;
color: white;
background: rgba(0, 0, 0, 0.5);
padding: 0.5em;
}
<canvas></canvas>
<div id="info">use cursor left/right</div>
Note: it might be better to skip ctx.save and ctx.restore and instead use ctx.setTransform for speed.

HTML Canvas Trying to create an animated chain of rectangle with slight delay/distance between them

I am trying to create multiple animated rectangles using Html Canvas with requestAnimationFrame. As for now, I managed to do exactly what I wanted with only one animated rectangle, but I can't find out how to create more rectangles that would simply be in line and follow each other with an equal distance.
Also, there's a random data (I, II or III) inside each rectangle.
Here's my actual code:
//Referencing canvas
var canvas = document.getElementById("my-canvas");
var ctx = canvas.getContext("2d");
//Make Canvas fullscreen and responsive
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
window.addEventListener('resize', resize, false); resize();
//FPS
var framesPerSecond = 60;
//Default Y pos to center;
var yPos = canvas.height / 2;
//Default X pos offset
var xPos = -150;
//Speed (increment)
var speed = 2;
//Our array to store rectangles objects
var rectangles = [] ;
//Dynamic Number from database
var quote = ["I", "II", "III"];
//Random number for testing purpose
var rand = quote[Math.floor(Math.random() * quote.length)];
//Draw Rectangle
function drawRectangle () {
setTimeout(function() {
requestAnimationFrame(drawRectangle);
ctx.clearRect(0, 0, canvas.width, canvas.height);
//Background color
ctx.fillStyle = "yellow";
//Position, size.
var rectWidth = 70;
var rectHeigth = 55;
ctx.fillRect(xPos,yPos,rectWidth,rectHeigth);
ctx.font = "32px Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = "black";
//Data Layer
var dataLayer = ctx.fillText(rand,xPos+(rectWidth/2),yPos+(rectHeigth/2));
xPos += speed;
//Infinite loop for test
if (xPos > 1080) {
xPos = -150;
}
}, 1000 / framesPerSecond);
}
drawRectangle ();
canvas {background-color: #131217}
body { margin: 0; overflow: hidden; }
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Moving Blocks</title>
<style>
canvas {background-color: #131217}
body { margin: 0; overflow: hidden; }
</style>
</head>
<body>
<canvas id="my-canvas"></canvas>
</body>
</html>
Animating arrays of objects.
For animations you are best of using a single render function that renders all the objects once a frame, rather than create a separate render frame per object.
As for the squares there are many ways that you can get them to do what you want. It is a little difficult to answer as what you want is not completely clear.
This answer will use a rectangle object that has everything needed to be rendered and move. The rectangles will be kept in an array and the main render function will update and render each rectangle in turn.
There will be a spawn function that creates rectangles untill the limit has been reached.
// constants up the top
const quote = ["I", "II", "III"];
// function selects a random Item from an array
const randItem = (array) => array[(Math.random() * array.length) | 0];
// array to hold all rectangles
const rectangles = [];
var maxRectangles = 20;
const spawnRate = 50; // number of frames between spawns
var spawnCountdown = spawnRate;
//Referencing canvas
const ctx = canvas.getContext("2d");
var w, h; // global canvas width and height.
resizeCanvas(); // size the canvas to fit the page
requestAnimationFrame(mainLoop); // this will start when all code below has been run
function mainLoop() {
// resize in the rendering frame as using the resize
// event has some issuse and this is more efficient.
if (w !== innerWidth || h !== innerHeight) {
resizeCanvas();
}
ctx.clearRect(0, 0, w, h);
spawnRectangle(); // spawns rectangles
updateAllRectangles(); // moves all active rectangles
drawAllRectangles(); // I will let you gues what this does... :P
requestAnimationFrame(mainLoop);
}
function resizeCanvas() {
w = canvas.width = innerWidth;
h = canvas.height = innerHeight;
// and reset any canvas constants
ctx.font = "32px Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
}
// function to spawn a rectangle
function spawnRectangle() {
if (rectangles.length < maxRectangles) {
if (spawnCountdown) {
spawnCountdown -= 1;
} else {
rectangles.push(
createRectangle({
y: canvas.height / 2, // set at center
text: randItem(quote),
dx: 2, // set the x speed
})
);
spawnCountdown = spawnRate;
}
}
}
// define the default rectangle
const rectangle = {
x: -40, // this is the center of the rectangle
y: 0,
dx: 0, // delta x and y are the movement per frame
dy: 0,
w: 70, // size
h: 55,
color: "yellow",
text: null,
textColor: "black",
draw() { // function to draw this rectangle
ctx.fillStyle = this.color;
ctx.fillRect(this.x - this.w / 2, this.y - this.h / 2, this.w, this.h);
ctx.fillStyle = this.textColor;
ctx.fillText(this.text, this.x, this.y);
},
update() { // moves the rectangle
this.x += this.dx;
this.y += this.dy;
if (this.x > canvas.width + this.w / 2) {
this.x = -this.w / 2;
// if the rectangle makes it to the other
// side befor all rectangles are spawnd
// then reduce the number so you dont get any
// overlap
if (rectangles.length < maxRectangles) {
maxRectangles = rectangles.length;
}
}
}
}
// creats a new rectangle. Setting can hold any unique
// data for the rectangle
function createRectangle(settings) {
return Object.assign({}, rectangle, settings);
}
function updateAllRectangles() {
var i;
for (i = 0; i < rectangles.length; i++) {
rectangles[i].update();
}
}
function drawAllRectangles() {
var i;
for (i = 0; i < rectangles.length; i++) {
rectangles[i].draw();
}
}
canvas {
position: absolute;
top: 0px;
left: 0px;
background-color: #131217;
}
body {
margin: 0;
overflow: hidden;
}
<canvas id="canvas"></canvas>

Scribing a cicrular line in Canvas

I am attempting to mouse-drag a dot around the outer perimeter of a large circle and have that dot appear to scribe a thick line around the outer perimeter behind itself. I can get everything to work except scribing the outer line behind the dot. I have researched many ideas and tried many of my own but the line still produces "spotted" results. Here is an image to show what I'm attempting.
MounseDrag Scribed Line
Thank you for taking the time to read my question. :-)
<script type="text/javascript">
var canvas1 = document.getElementById("canvas1"),
canvas2 = document.getElementById("canvas2"),
c1 = canvas1.getContext("2d"),
c2 = canvas2.getContext("2d"),
dot = 7,
started = false,
width = 350,
height = 350,
radians = 0,
cRad = 165, // Circle Radius
cord = {mX:0, mY:0, csX:0, snY:0, x:0, y:0},
init = function(){
cord.mX = 0;
cord.mY = 0;
cord.csX = width /2 + cRad;
cord.snY = height /2;
cord.x = width /2;
cord.y = height /2;
};
init();
canvas1.width = width;
canvas1.height = height;
canvas2.width = width;
canvas2.height = height;
canvas1.addEventListener("mousemove", function(event) {
cord.mX = event.clientX - canvas1.offsetLeft;
cord.mY = event.clientY - canvas1.offsetTop;
});
canvas1.addEventListener("mousedown", function(event) {
if (started) {
started = false;
} else {
started = true;
render();
};
});
function update() {
radians = Math.atan2(cord.mY - width/2, cord.mX - height/2);
cord.csX = width/2 - Math.cos(radians) * cRad * -1;
cord.snY = height/2 - Math.sin(radians) * cRad * -1;
};
function outerTheta() {
c2.beginPath();
c2.arc(cord.csX, cord.snY, 3, 0, Math.PI * 2);
c2.closePath();
c2.fillStyle = "#000";
c2.fill();
};
function render() {
c1.clearRect(0, 0, width, height);
c1.beginPath();
c1.moveTo(cord.x, cord.y);
c1.lineTo(cord.csX, cord.snY);
c1.lineWidth = 3;
c1.strokeStyle = "#000";
c1.stroke();
c1.beginPath(); //<---------------------------------- Drag-Dot
c1.arc(cord.csX, cord.snY, dot, 0, Math.PI * 2);
c1.closePath();
c1.fillStyle = "#000";
c1.fill();
if(started){
update();
outerTheta();
requestAnimationFrame(render);
};
};
render();
</script>
The browser is not able to cycle the animation as quickly as the mouse is moving. If you move the mouse slowly, then the dots that are drawn in each animation cycle overlap and the circle has a solid line. If you move the mouse quickly, then the dots do not overlap and you get "spotting".
If you pay close attention to the way drawing programs work, you will see that the "pen" tool draws a continuous line. If you move the mouse quickly while using the tool, the continuous line is made up of line segments that stretch from each point that the computer was able to capture while your mouse was moving quickly.
I modified your program so that a line segment stretches between each captured point during the animation cycle:
https://jsfiddle.net/17hvw5pp
var canvas1 = document.getElementById("canvas1"),
canvas2 = document.getElementById("canvas2"),
c1 = canvas1.getContext("2d"),
c2 = canvas2.getContext("2d"),
dot = 7,
started = false,
width = 350,
height = 350,
radians = 0,
cRad = 165, // Circle Radius
cord = {mX:0, mY:0, csX:0, snY:0, x:0, y:0},
init = function(){
cord.mX = 0;
cord.mY = 0;
cord.csX = width /2 + cRad;
cord.snY = height /2;
cord.lastCSX = cord.csX;
cord.lastSNY = cord.snY;
cord.x = width /2;
cord.y = height /2;
};
canvas1.style.position="absolute";
canvas2.style.position="absolute";
init();
canvas1.width = width;
canvas1.height = height;
canvas2.width = width;
canvas2.height = height;
canvas1.addEventListener("mousemove", function(event) {
cord.mX = event.clientX - canvas1.offsetLeft;
cord.mY = event.clientY - canvas1.offsetTop;
});
canvas1.addEventListener("mousedown", function(event) {
if (started) {
started = false;
} else {
started = true;
render();
};
});
function update() {
radians = Math.atan2(cord.mY - width/2, cord.mX - height/2);
cord.csX = width/2 - Math.cos(radians) * cRad * -1;
cord.snY = height/2 - Math.sin(radians) * cRad * -1;
};
function outerTheta() {
//draw a line from the last known coordinate to the new known coordinate
c2.beginPath();
c2.moveTo(cord.lastCSX, cord.lastSNY);
c2.lineTo(cord.csX, cord.snY);
c2.lineWidth=5;
c2.strokeStyle="#000";
c2.stroke();
cord.lastCSX = cord.csX;
cord.lastSNY = cord.snY;
c2.beginPath();
c2.arc(cord.csX, cord.snY, 3, 0, Math.PI * 2);
c2.closePath();
c2.fillStyle = "#000";
c2.fill();
};
function render() {
c1.clearRect(0, 0, width, height);
c1.beginPath();
c1.moveTo(cord.x, cord.y);
c1.lineTo(cord.csX, cord.snY);
c1.lineWidth = 3;
c1.strokeStyle = "#000";
c1.stroke();
c1.beginPath(); //<---------------------------------- Drag-Dot
c1.arc(cord.csX, cord.snY, dot, 0, Math.PI * 2);
c1.closePath();
c1.fillStyle = "#000";
c1.fill();
if(started){
update();
outerTheta();
requestAnimationFrame(render);
};
};
render();
This works better, but not perfectly: If you move the mouse quickly, the line segment will become a chord across the circle and this ruins the effect.
I attempted to modify the program to draw an arc between the two known points:
https://jsfiddle.net/17hvw5pp/1/
You can see that this implementation is also not ideal because the arc function becomes confused about which direction to draw the partial circle based on just two radians coordinates. Using quaternion math will solve this problem for you.
https://en.wikipedia.org/wiki/Quaternion
But that may be more complication that you want to introduce into this project.

Categories

Resources