This question already has answers here:
Canvas is stretched when using CSS but normal with "width" / "height" properties
(10 answers)
Real mouse position in canvas
(5 answers)
Closed 6 months ago.
need help!!! i am following this new javascript 2D gaming video and i am currently stuck on 2:32:29 where we are making an explosion effect on canvas where the mouse clicked. https://www.youtube.com/watch?v=GFO_txvwK_c&t=9365s.
problem: whenever i click on the canvas, no sqaure shapes shows up, in the tutorial it mentioned you will have to offset the e.x and e.y to make it relative the canvas. but i realized whenever i click the top left corner of the screen. sometimes an elongated rectangular shape does appear but when i offset my e.x and e.y. i can not get the correct offset to make it work. could anyone help explain why i am getting this weird offset effect on my event.x and event.y values and how to fix it? thanks a lot!!! you can see the thrid picture attached for my code adding the offset suggested by video and it still does not work.
const canvas=document.getElementById('canvas1');
const ctx = canvas.getContext('2d');
canvas.width = 500;
canvas,height = 700;
const explosion =[];
class Explosion {
constructor(x,y){
this.x = x;
this.y = y;
this.spriteWidth = 200;
this.spriteHeight = 179;
this.width = this.spriteWidth/2;
this.height = this.spriteHeight/2;
this.image = new this.image();
this.image.src = "boom.png";
this.frame=0;
}
update(){
this.frame++;
}
draw(){
ctx.drawImage(this.image,this.spriteWidth*this.frame,0,this.spriteWidth,this.spriteHeight,this.x,this.y,this.width, this.height);
}
}
window.addEventListener('click', function(e){
ctx.fillStyle= 'white';
ctx.fillRect(e.x,e.y,50,50);
})
enter image description here
enter image description here
enter image description here
First, I wouldn't attach the click listener to the global window object. As your merely interested in clicks happening inside the <canvas> element, attach it to this instead.
Second, the x and y properties returned by the mouseevent don't match what you might expect because it does not take into account the offset of the HTML element on the page. Try using the offsetX and offsetY properties instead.
Here's an example:
const canvas = document.getElementById('canvas1');
const ctx = canvas.getContext('2d');
canvas.width = 500;
canvas.height = 700;
const explosion = [];
class Explosion {
constructor(x, y) {
this.x = x;
this.y = y;
this.spriteWidth = 200;
this.spriteHeight = 179;
this.width = this.spriteWidth / 2;
this.height = this.spriteHeight / 2;
this.image = new this.image();
this.image.src = "boom.png";
this.frame = 0;
}
}
canvas.addEventListener('click', function(e) {
ctx.fillStyle = 'blue';
ctx.fillRect(e.offsetX, e.offsetY, 50, 50);
});
<canvas id="canvas1" style="background: #eeeeee"></canvas>
I'm creating a canvas with an overlay div to add markers on click and I want markers to change position when I pan zoom the canvas or resize the window. I'm using https://github.com/timmywil/panzoom to pan zoom.
The problem is when I convert mouse position to canvas coordinates it worked correctly but when I convert it back to screen position to render markers on overlay div, the result is not as same as initialized mouse position and recalculate marker's position on resize also not correct.
This canvas is fullscreen with no scroll.
width = 823; height = 411;
scale = 2; panX = 60; panY = 10;
mouse.pageX = 467; mouse.pageY = 144;
// {x: 475, y: 184} correct coords when I use ctx.drawImage(..) to test
canvasCoords = getCanvasCoords(mouse.pageX, mouse.pageY, scale);
// {x: 417, y: 124}
screenCoords = toScreenCoords(canvasCoords.x, canvasCoords.y, scale, panX, panY);
------------------------------
but with scale = 1; it worked correctly.
// convert mouse position to canvas coordinates
getCanvasCoords(pageX: number, pageY: number, scale: number) {
var rect = this.pdfInfo.canvas.getBoundingClientRect();
let x = (pageX - rect.left + this.scrollElement.scrollTop) / scale;
let y = (pageY - rect.top + this.scrollElement.scrollLeft) / scale;
return {
x: Number.parseInt(x.toFixed(0)),
y: Number.parseInt(y.toFixed(0)),
};
}
// convert canvas coords to screen coords
toScreenCoords(
x: number,
y: number,
scale: number
) {
var rect = this.pdfInfo.canvas.getBoundingClientRect();
let wx =
x * scale + rect.left - this.scrollElement.scrollTop / scale;
let wy =
y * scale + rect.top - this.scrollElement.scrollLeft / scale;
return {
x: Number.parseInt(wx.toFixed(0)),
y: Number.parseInt(wy.toFixed(0)),
};
}
getNewPos(x, oldV, newV) {
return (x * oldV) / newV;
}
// update screen coords with new screen width and height
onResize(old, new) {
this.screenCoordList.forEach(el => {
el.x = getNewPos(el.x, old.width, new.width);
el.y = getNewPos(el.y, old.height, new.height);
})
}
How to get it worked with scale and pan? if you know any library can do the job please recommend, thank you.
Here's a code snippet that seems to be working, you can probably adapt it for your purposes.
What I used was:
function toCanvasCoords(pageX, pageY, scale) {
var rect = canvas.getBoundingClientRect();
let x = (pageX - rect.left) / scale;
let y = (pageY - rect.top) / scale;
return toPoint(x, y);
}
and
function toScreenCoords(x, y, scale) {
var rect = canvas.getBoundingClientRect();
let wx = x * scale + rect.left + scrollElement.scrollLeft;
let wy = y * scale + rect.top + scrollElement.scrollTop;
return toPoint(wx, wy);
}
I'm just getting the mouse position from the window object. I'm may be mistaken, but I think this is why scrollLeft and scrollTop don't appear in toCanvasCoords (since the position is relative to the client area of the window itself, the scroll doesn't come into it). But then when you transform back, you have to take it into account.
This ultimately just returns the mouse position relative to the window (which was the input), so it's not really necessary to do the whole transformation in a roundabout way if you just want to attach an element to the mouse pointer. But transforming back is useful if you want to have something attached to a certain point on the canvas image (say, a to feature on the map) - which I'm guessing is something that you're going for, since you said that you want to render markers on an overlay div.
In the code snippet bellow, the red circle is drawn on the canvas itself at the location returned by toCanvasCoords; you'll notice that it scales together with the background.
I didn't use an overlay div covering the entire map, I just placed a couple of small divs on top using absolute positioning. The black triangle is a div (#tracker) that basically tracks the mouse; it is placed at the result of toScreenCoords. It serves as a way to check if the transformations work correctly. It's an independent element, so it doesn't scale with the image.
The red triangle is another such div (#feature), and demonstrates the aforementioned "attach to feature" idea. Suppose the background is a something like a map, and suppose you want to attach a "map pin" icon to something on it, like to a particular intersection; you can take that location on the map (which is a fixed value), and pass it to toScreenCoords. In the code snippet below, I've aligned it with a corner of a square on the background, so that you can track it visually as you change scale and/or scroll. (After you click "Run code snippet", you can click "Full page", and then resize the window to get the scroll bars).
Now, depending on what exactly is going on in your code, you may have tweak a few things, but hopefully, this will help you. If you run into problems, make use of console.log and/or place some debug elements on the page that will display values live for you (e.g. mouse position, client rectangle, etc.), so that you can examine values. And take things one step at the time - e.g. first get the scale to work, but ignore scrolling, then try to get scrolling to work, but keep the scale at 1, etc.
const canvas = document.getElementById('canvas');
const context = canvas.getContext("2d");
const tracker = document.getElementById('tracker');
const feature = document.getElementById('feature');
const slider = document.getElementById("scale-slider");
const scaleDisplay = document.getElementById("scale-display");
const scrollElement = document.querySelector('html');
const bgImage = new Image();
bgImage.src = "https://i.stack.imgur.com/yxtqw.jpg"
var bgImageLoaded = false;
bgImage.onload = () => { bgImageLoaded = true; };
var mousePosition = toPoint(0, 0);
var scale = 1;
function updateMousePosition(evt) {
mousePosition = toPoint(evt.clientX, evt.clientY);
}
function getScale(evt) {
scale = evt.target.value;
scaleDisplay.textContent = scale;
}
function toCanvasCoords(pageX, pageY, scale) {
var rect = canvas.getBoundingClientRect();
let x = (pageX - rect.left) / scale;
let y = (pageY - rect.top) / scale;
return toPoint(x, y);
}
function toScreenCoords(x, y, scale) {
var rect = canvas.getBoundingClientRect();
let wx = x * scale + rect.left + scrollElement.scrollLeft;
let wy = y * scale + rect.top + scrollElement.scrollTop;
return toPoint(wx, wy);
}
function toPoint(x, y) {
return { x: x, y: y }
}
function roundPoint(point) {
return {
x: Math.round(point.x),
y: Math.round(point.y)
}
}
function update() {
context.clearRect(0, 0, 500, 500);
context.save();
context.scale(scale, scale);
if (bgImageLoaded)
context.drawImage(bgImage, 0, 0);
const canvasCoords = toCanvasCoords(mousePosition.x, mousePosition.y, scale);
drawTarget(canvasCoords);
const trackerCoords = toScreenCoords(canvasCoords.x, canvasCoords.y, scale);
updateTrackerLocation(trackerCoords);
updateFeatureLocation()
context.restore();
requestAnimationFrame(update);
}
function drawTarget(location) {
context.fillStyle = "rgba(255, 128, 128, 0.8)";
context.beginPath();
context.arc(location.x, location.y, 8.5, 0, 2*Math.PI);
context.fill();
}
function updateTrackerLocation(location) {
const canvasRectangle = offsetRectangle(canvas.getBoundingClientRect(),
scrollElement.scrollLeft, scrollElement.scrollTop);
if (rectContains(canvasRectangle, location)) {
tracker.style.left = location.x + 'px';
tracker.style.top = location.y + 'px';
}
}
function updateFeatureLocation() {
// suppose the background is a map, and suppose there's a feature of interest
// (e.g. a road intersection) that you want to place the #feature div over
// (I roughly aligned it with a corner of a square).
const featureLoc = toScreenCoords(84, 85, scale);
feature.style.left = featureLoc.x + 'px';
feature.style.top = featureLoc.y + 'px';
}
function offsetRectangle(rect, offsetX, offsetY) {
// copying an object via the spread syntax or
// using Object.assign() doesn't work for some reason
const result = JSON.parse(JSON.stringify(rect));
result.left += offsetX;
result.right += offsetX;
result.top += offsetY;
result.bottom += offsetY;
result.x = result.left;
result.y = result.top;
return result;
}
function rectContains(rect, point) {
const inHorizontalRange = rect.left <= point.x && point.x <= rect.right;
const inVerticalRange = rect.top <= point.y && point.y <= rect.bottom;
return inHorizontalRange && inVerticalRange;
}
window.addEventListener('mousemove', (e) => updateMousePosition(e), false);
slider.addEventListener('input', (e) => getScale(e), false);
requestAnimationFrame(update);
#canvas {
border: 1px solid gray;
}
#tracker, #feature {
position: absolute;
left: 0;
top: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-bottom: 10px solid black;
transform: translate(-4px, 0);
}
#feature {
border-bottom: 10px solid red;
}
<div>
<label for="scale-slider">Scale:</label>
<input type="range" id="scale-slider" name="scale-slider" min="0.5" max="2" step="0.02" value="1">
<span id="scale-display">1</span>
</div>
<canvas id="canvas" width="500" height="500"></canvas>
<div id="tracker"></div>
<div id="feature"></div>
P.S. Don't do Number.parseInt(x.toFixed(0)); generally, work with floating point for as long as possible to minimize accumulation of errors, and only convert to int at the last minute. I've included the roundPoint function that rounds the (x, y) coordinates of a point to the nearest integer (via Math.round), but ended up not needing to use it at all.
Note: The image below is used as the background in the code snippet, to serve as a reference point for scaling; it is included here just so that it is hosted on Stack Exchange's imgur.com account, so that the code is not referencing a (potentially volatile) 3rd-pary source.
I use a system that combine a div background image and a canvas. This system let me appear the background like there is a light like this :
I permit to the user to zoom in/out the background. But i need to keep every drawn element positions. Today i have this :
After zoom in :
The wall height and width are correctly resize but its position move with the scale.
For the code i have this for the background :
document.getElementById("background").style.transform = "translate("+backgroundTransform.x+"px,"+backgroundTransform.y+"px) scale("+backgroundTransform.zoom+","+backgroundTransform.zoom+")";
And this for the drawn element :
this.x = function(x){
if(x != undefined)
this.position.x = x;
return this.position.x + backgroundTransform.x;
},
this.y = function(y){
if(y != undefined)
this.position.y = y;
return this.position.y + backgroundTransform.y;
},
this.width = function() {return this.baseWidth * this.globalZoom()};
this.height = function() {return this.baseHeight * this.globalZoom()};
If you need any informations please let me know.
Thanks.
I switch the div background with a canvas background. Now they can scale at the same time.
I am trying to animate using a sprite sheet and use it in my website, however I am knew to animation and I am having some trouble fixing it, currently nothing shows up on my webpage when I use this. I am using a sprite sheet which is called Speech.png, is 18176 x 256 and each frame is 256x256. Here is my code so far.
<script language="javascript">
// screen size variables
var SCREEN_WIDTH = window.innerWidth,
SCREEN_HEIGHT = window.innerHeight;
var canvas = document.createElement('canvas');
var c = canvas.getContext('2d');
canvas.width = SCREEN_WIDTH;
canvas.height = SCREEN_HEIGHT;
var xpos=0,
ypos=0,
index=0,
numFrames = 70,
frameSize= 255;
// Add our drawing canvas
document.body.appendChild(canvas);
//load the image
image = new Image();
image.src = "Speech.png";
image.onload = function() {
//we're ready for the loop
setInterval(loop, 1000 / 30);
}
function loop() {
//clear the canvas!
c.clearRect(0,0, SCREEN_HEIGHT,SCREEN_WIDTH);
/*our big long list of arguments below equates to:
1: our image source
2 - 5: the rectangle in the source image of what we want to draw
6 - 9: the rectangle of our canvas that we are drawing into
the area of the source image we are drawing from will change each time loop() is called.
the rectangle of our canvas that we are drawing into however, will not.
tricky!
*/
c.drawImage(image,xpos,ypos,frameSize,frameSize,0,0,frameSize, frameSize);
//each time around we add the frame size to our xpos, moving along the source image
xpos += frameSize;
//increase the index so we know which frame of our animation we are currently on
index += 1;
//if our index is higher than our total number of frames, we're at the end and better start over
if (index >= numFrames) {
xpos = 0;
ypos = 0;
index = 0;
//if we've gotten to the limit of our source image's width, we need to move down one row of frames
} else if (xpos + frameSize > image.width){
xpos =0;
ypos += frameSize;
}
}
</script>
I am using the Panzoom.js plugin to pan and zoom an element. There is a mask applied to the cursor, the cursor reveals a portion of the underlying image wherever it hovers over the image(imagine a black and white image on top of a colored image, the cursor would reveal a portion of the colored image where ever the cursor hovers over the image). When i zoom in on the image in Chrome and Safari(using Panzoom), the mouse coordinates are given properly and thus the mask lines up properly with the cursor. However, in Firefox and IE while zooming in the cursor coordinates are not correct and thus the hover mask doesn't align properly with the cursor. Am I missing something obvious? Thanks for looking into this. Below is the code.
var svg = document.querySelector("svg");
var pt = svg.createSVGPoint();
function cursorPoint(evt) {
pt.x = evt.clientX;
pt.y = evt.clientY;
return pt.matrixTransform(svg.getScreenCTM().inverse());
}
function getCanvasCoords(x, y) {
var matrix = $panzoom.panzoom("getMatrix");
var calc_x = x * (1 / matrix[0]);
var calc_y = y * (1 / matrix[3]);
return {
x: calc_x,
y: calc_y
};
}
$("#parentID").mousemove(function (event) {
event.preventDefault();
var loc = cursorPoint(event);
var img = document.getElementById("parentID");
var imgPos = img.getBoundingClientRect();
var x = loc.x - imgPos.left;
var y = loc.y - imgPos.top;
var coords = getCanvasCoords(x, y);
primaryCircle.setAttribute("cy", (coords.y) + "px");
primaryCircle.setAttribute("cx", (coords.x) + "px");
});
Use Math.round
function getCanvasCoords(x, y) {
var matrix = $panzoom.panzoom("getMatrix");
var calc_x = x * Math.round((1 / matrix[0]));
var calc_y = y * Math.round(1 / matrix[3]);
return {
x: calc_x,
y: calc_y
};
}