When a mouse is hovering a image. It gets detect by this if statement:
if ((distance(circles[this.index].x, circles[this.index].y, mouse.x, mouse.y)) < circles[this.index].radius)
I also want to detect when a mouse it outside a image.
After that previous if statement I cannot use else the reason is because:
When I generate multiple images on screen and when my mouse if hovering over 1 image. It does hover of that image and the code detects it but it also doesnt hover of all the other images. That is the reason that is display 4 times "outside circle" and 1 time "inside circle"
As seen in the log:
Console.log output:
Mouse inside circle
Mouse outside circle 4
Mouse inside circle
Mouse outside circle 4
Im looking for a way the detect when the mouse is leaving a circle.
You can find the code I'm working with below:
PS: it it important that it detect in what (index) circle the mouse is and leaves.
I want to create a huge amount of pictures, but in the code below I used 5 for demo purpeses.
var mouse = {
x: innerWidth / 2,
y: innerHeight / 2
};
// Mouse Event Listeners
addEventListener('mousemove', event => {
mouse.x = event.clientX;
mouse.y = event.clientY;
});
//Calculate distance between 2 objects
function distance(x1, y1, x2, y2) {
let xDistance = x2 - x1;
let yDistance = y2 - y1;
return Math.sqrt(Math.pow(xDistance, 2) + Math.pow(yDistance, 2));
}
// Sqaure to circle
function makeCircleImage(radius, src, callback) {
var canvas = document.createElement('canvas');
canvas.width = canvas.height = radius * 2;
var ctx = canvas.getContext("2d");
var img = new Image();
img.src = src;
img.onload = function() {
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
// we use compositing, offers better antialiasing than clip()
ctx.globalCompositeOperation = 'destination-in';
ctx.arc(radius, radius, radius, 0, Math.PI*2);
ctx.fill();
callback(canvas);
};
}
function Circle( x, y, radius, index ) {
//Give var for circle
this.x = x;
this.y = y;
this.dx = 1;
this.dy = 1;
this.radius = radius;
this.index = index;
}
// use prototyping if you wish to make it a class
Circle.prototype = {
//Draw circle on canvas
draw: function () {
var
x = (this.x - this.radius),
y = (this.y - this.radius);
// draw is a single call
c.drawImage( this.image, x, y );
},
//Updates position of images
update: function () {
var
max_right = canvas.width + this.radius,
max_left = this.radius * -1;
this.x += this.dx;
if( this.x > max_right ) {
this.x += max_right - this.x;
this.dx *= -1;
}
if( this.x < max_left ) {
this.x += max_left - this.x;
this.dx *= -1;
}
if ((distance(circles[this.index].x, circles[this.index].y, mouse.x, mouse.y)) < circles[this.index].radius) {
// Mouse inside circle
console.log("Mouse inside circle")
} else{
//The mouse is in one circle
//And out of 4 other circles
console.log("Mouse outside circle")
}
},
init: function(callback) {
var url = "https://t4.ftcdn.net/jpg/02/26/96/25/240_F_226962583_DzHr45pyYPdmwnjDoqz6IG7Js9AT05J4.jpg";
makeCircleImage( this.radius, url, function(img) {
this.image = img;
callback();
}.bind(this));
}
};
//Animate canvas
function animate() {
c.clearRect(0, 0, window.innerWidth, window.innerHeight);
circles.forEach(function( circle ) {
circle.update();
});
circles.forEach(function( circle ) {
circle.draw();
});
requestAnimationFrame(animate);
}
//Init canvas
var canvas = document.querySelector('canvas');
var c = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
//init circle objects
var circles = [
new Circle(10, 100, 50,0),
new Circle(10, 200, 30,1),
new Circle(10, 300, 50,2),
new Circle(10, 400, 50,3),
new Circle(10, 500, 50,4)
];
var ready = 0;
circles.forEach(function(circle) {
circle.init(oncircledone);
});
function oncircledone() {
if(++ready === circles.length) {
animate()
}
}
<canvas></canvas>
just add another property to circle
function Circle(x, y, radius, index) {
//Give var for circle
this.x = x;
this.y = y;
this.dx = 1;
this.dy = 1;
this.radius = radius;
this.index = index;
this.mouseInside = false
}
and then the update logic change to this
if ((distance(this.x, this.y, mouse.x, mouse.y)) < circles[this.index].radius) {
if (!this.mouseInside) {
this.mouseInside = true
console.log(`mouse enter circele at ${this.index}`)
}
}
else if (this.mouseInside) {
this.mouseInside = false
console.log(`mouse leave circele at ${this.index}`)
}
check if circles overlap and the you can decide if you want to update
var overlapsCircles = circles.filter(circle => {
var diffrentId = circle.index != this.index
var overlapping =
distance(this.x, this.y, circle.x, circle.y) < this.radius
return diffrentId && overlapping
})
if (overlapsCircles.length > 0) {
var overlapCircle = overlapsCircles.map(circle => circle.index)
console.log('overlap circle with index ' + overlapCircle)
}
var mouse = {
x: innerWidth / 2,
y: innerHeight / 2
};
// Mouse Event Listeners
addEventListener('mousemove', event => {
mouse.x = event.clientX;
mouse.y = event.clientY;
});
//Calculate distance between 2 objects
function distance(x1, y1, x2, y2) {
let xDistance = x2 - x1;
let yDistance = y2 - y1;
return Math.sqrt(Math.pow(xDistance, 2) + Math.pow(yDistance, 2));
}
// Sqaure to circle
function makeCircleImage(radius, src, callback) {
var canvas = document.createElement('canvas');
canvas.width = canvas.height = radius * 2;
var ctx = canvas.getContext("2d");
var img = new Image();
img.src = src;
img.onload = function () {
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
// we use compositing, offers better antialiasing than clip()
ctx.globalCompositeOperation = 'destination-in';
ctx.arc(radius, radius, radius, 0, Math.PI * 2);
ctx.fill();
callback(canvas);
};
}
function Circle(x, y, radius, index) {
//Give var for circle
this.x = x;
this.y = y;
this.dx = 1;
this.dy = 1;
this.radius = radius;
this.index = index;
this.mouseInside = false
}
// use prototyping if you wish to make it a class
Circle.prototype = {
//Draw circle on canvas
draw: function () {
var
x = (this.x - this.radius),
y = (this.y - this.radius);
// draw is a single call
c.drawImage(this.image, x, y);
},
//Updates position of images
update: function () {
var
max_right = canvas.width + this.radius,
max_left = this.radius * -1;
this.x += this.dx;
if (this.x > max_right) {
this.x += max_right - this.x;
this.dx *= -1;
}
if (this.x < max_left) {
this.x += max_left - this.x;
this.dx *= -1;
}
if ((distance(this.x, this.y, mouse.x, mouse.y)) < circles[this.index].radius) {
if (!this.mouseInside) {
this.mouseInside = true
console.log(`mouse enter circele at ${this.index}`)
}
}
else if (this.mouseInside) {
this.mouseInside = false
console.log(`mouse leave circele at ${this.index}`)
}
},
init: function (callback) {
var url = "https://t4.ftcdn.net/jpg/02/26/96/25/240_F_226962583_DzHr45pyYPdmwnjDoqz6IG7Js9AT05J4.jpg";
makeCircleImage(this.radius, url, function (img) {
this.image = img;
callback();
}.bind(this));
}
};
//Animate canvas
function animate() {
c.clearRect(0, 0, window.innerWidth, window.innerHeight);
circles.forEach(function (circle) {
circle.update();
});
circles.forEach(function (circle) {
circle.draw();
});
requestAnimationFrame(animate);
}
//Init canvas
var canvas = document.querySelector('canvas');
var c = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
//init circle objects
var circles = [
new Circle(10, 100, 50, 0),
new Circle(10, 200, 30, 1),
new Circle(10, 300, 50, 2),
new Circle(10, 400, 50, 3),
new Circle(10, 500, 50, 4)
];
var ready = 0;
circles.forEach(function (circle) {
circle.init(oncircledone);
});
function oncircledone() {
if (++ready === circles.length) {
animate()
}
}
<canvas id="ctx"></canvas>
Ambiguities
It is not clear what you need in regard to circles and some point (in this answer point is a substitute for mouse and only requires that it have the properties x and y to be valid ).
The lack of information in your question concerns the facts
that many circles can be under the point at the same time.
and that more than one circle can move from under to out or out to under the point per frame.
the wording of the question suggest you are after just one circle which conflicts with the above 2 concerns.
Assumptions
I will assume that the interaction with the circles are more than just a simple on under event like interaction. That they may include animation related behaviors that are triggered by the state related to the point.
I assume that the visual order of the circles will determine how you select circles of interest.
That all circles per frame that meet the required conditions and can be accessed quickly.
That performance is important as you wish to have many circles that interact with a point.
That there is only one point (mouse, touch, other source) per frame that interacts with the circles
There is no requirement for circle circle interaction
Solution
The example below covers the above assumptions and resolves any ambiguities in the question. It is designed to be efficient and flexible.
The circles are stored in an array that has had its properties extended called circles
Rendering and state sets
The function circles.updateDraw(point) updates and draws all the circles. The argument point is a point to check the circle against. It defaults to the mouse.
All circles are drawn with an outline. Circles under the point (eg mouse) are filled with green, Circles just moved to under the point (eg onMouseOver) are filled with yellow, circle that have just move out from under are filled with red.
There are 3 arrays as properties of circles that contain circles as define...
circles.under All circles under the point
circles.outFromUnder All circles just out from under the point
circles.newUnder All circles new to under the point
These array are populated by the function circles.updateDraw(point)
Query all circles point state
Circles also have 3 functions that refer to the above arrays as set the default set is circles.under.
The functions are..
circles.firstInSet(set) Returns the first circle (The visual bottom most) in set or undefined
circles.lastInSet(set) Returns the last circle (The visual top most) in set or undefined
circles.closestInSet(set) Returns the closest circle to the point in set or undefined
For example to get the visual top most circle just under the mouse you would call circles.lastInSet(circles.newUnder) or to get the circle closest to the mouse from all circles under the mouse you would call circles.closestInSet(circles.newUnder) (or as it defaults to set under call circles.closestInSet() )
Circle additional states
Each Circle has some additional properties.
Circle.distSqr is the square of the distance from the point
Circle.rSqr is the square of the radius calculated when constructed.
Circle.underCount This value can be used to apply animations to the circle based on its relative state to the point.
If positive is the number of frames plus 1, the circle is under the point.
If this value is 1 then the circle is just moved from not under to under.
If this value is 0 the it has just moved out from under the point.
If negative this value is the number of frames the circle is not under the point
Running Demo
Use mouse to move over circles.
The circle closest and under the mouse is filled with white with alpha = 0.5
addEventListener('mousemove', event => {
mouse.x = event.clientX;
mouse.y = event.clientY;
});
Math.TAU = Math.PI * 2;
Math.rand = (min, max) => Math.random() * (max - min) + min;
const CIRCLE_RADIUS = 50;
const UNDER_STYLE = "#0A0";
const NEW_UNDER_STYLE = "#FF0";
const OUT_STYLE = "#F00";
const CIRCLE_STYLE = "#000";
const CIRCLE_LINE_WIDTH = 1.5;
const CIRCLE_COUNT = 100;
const CIRCLE_CLOSEST = "#FFF";
const ctx = canvas.getContext('2d');
const mouse = {x: 0, y: 0};
requestAnimationFrame(() => {
sizeCanvas();
var i = CIRCLE_COUNT;
while (i--) {
const r = Math.rand(CIRCLE_RADIUS / 3, CIRCLE_RADIUS);
circles.push(new Circle(
Math.rand(r, canvas.width - r),
Math.rand(r, canvas.height - r),
Math.rand(-1, 1),
Math.rand(-1, 1),
r
));
}
animate()
});
function animate() {
sizeCanvas();
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
circles.updateDraw();
const c = circles.closestInSet(circles.under);
if(c) {
ctx.globalAlpha = 0.5;
ctx.beginPath();
ctx.fillStyle = CIRCLE_CLOSEST;
c.draw();
ctx.fill();
ctx.globalAlpha = 1;
}
requestAnimationFrame(animate);
}
function sizeCanvas() {
if (canvas.width !== innerWidth || canvas.height !== innerHeight) {
canvas.width = innerWidth;
canvas.height = innerHeight;
}
}
function Circle( x, y, dx = 0, dy = 0, radius = CIRCLE_RADIUS) {
this.x = x + radius;
this.y = y + radius;
this.dx = dx;
this.dy = dy;
this.radius = radius;
this.rSqr = radius * radius; // radius squared
this.underCount = 0; // counts frames under point
}
Circle.prototype = {
draw() {
ctx.moveTo(this.x + this.radius, this.y);
ctx.arc(this.x, this.y, this.radius, 0, Math.TAU);
},
update() {
this.x += this.dx;
this.y += this.dy;
if (this.x >= canvas.width - this.radius) {
this.x += (canvas.width - this.radius) - this.x;
this.dx = -Math.abs(this.dx);
} else if (this.x < this.radius) {
this.x += this.radius - this.x;
this.dx = Math.abs(this.dx);
}
if (this.y >= canvas.height - this.radius) {
this.y += (canvas.height - this.radius) - this.y;
this.dy = -Math.abs(this.dx);
} else if (this.y < this.radius) {
this.y += this.radius - this.y;
this.dy = Math.abs(this.dy);
}
},
isUnder(point = mouse) {
this.distSqr = (this.x - point.x) ** 2 + (this.y - point.y) ** 2; // distance squared
return this.distSqr < this.rSqr;
}
};
const circles = Object.assign([], {
under: [],
outFromUnder: [],
newUnder: [],
firstInSet(set = this.under) { return set[0] },
lastInSet(set = this.under) { return set[set.length - 1] },
closestInSet(set = this.under) {
var minDist = Infinity, closest;
if (set.length <= 1) { return set[0] }
for (const circle of set) {
if (circle.distSqr < minDist) {
minDist = (closest = circle).distSqr;
}
}
return closest;
},
updateDraw(point) {
this.under.length = this.newUnder.length = this.outFromUnder.length = 0;
ctx.strokeStyle = CIRCLE_STYLE;
ctx.lineWidth = CIRCLE_LINE_WIDTH;
ctx.beginPath();
for(const circle of this) {
circle.update();
if (circle.isUnder(point)) {
if (circle.underCount <= 0) {
circle.underCount = 1;
this.newUnder.push(circle);
} else { circle.underCount ++ }
this.under.push(circle);
} else if (circle.underCount > 0) {
circle.underCount = 0;
this.outFromUnder.push(circle);
} else {
circle.underCount --;
}
circle.draw();
}
ctx.stroke();
ctx.globalAlpha = 0.75;
ctx.beginPath();
ctx.fillStyle = UNDER_STYLE;
for (const circle of this.under) {
if (circle.underCount > 1) { circle.draw() }
}
ctx.fill();
ctx.beginPath();
ctx.fillStyle = OUT_STYLE;
for (const circle of this.outFromUnder) { circle.draw() }
ctx.fill();
ctx.beginPath();
ctx.fillStyle = NEW_UNDER_STYLE;
for (const circle of this.newUnder) { circle.draw() }
ctx.fill();
ctx.globalAlpha = 1;
}
});
#canvas {
position: absolute;
top: 0px;
left: 0px;
background: #6AF;
}
<canvas id="canvas"></canvas>
Well, the mouse is moving and you can simply create a Set which will contain circle objects that will store the circle(s) you are in:
let circleOfTrust = new Set();
//At the initialization you need to add any circles your point is currently in
and then at the loop:
circles.forEach(function( circle ) {
circleOfTrust[circle.update(circleOfTrust.has(circle)) ? "add" : "delete"](circle);
});
if (circleOfTrust.size() === 0) {
//point is outside the circles
} else {
//point is inside the circles in the set
}
and the update:
update: function (isInside) {
var
max_right = canvas.width + this.radius,
max_left = this.radius * -1;
this.x += this.dx;
if( this.x > max_right ) {
this.x += max_right - this.x;
this.dx *= -1;
}
if( this.x < max_left ) {
this.x += max_left - this.x;
this.dx *= -1;
}
return distance(circles[this.index].x, circles[this.index].y, mouse.x, mouse.y)) < circles[this.index].radius;
},
I would propose the following:
Keep a stack of figures with the order of how they were created (or any other meaningful order). This is needed to detect moves over overlapping figures.
Implement a function/method that iterates the stack and determines if the cursor is inside any of the figures.
Remember the last state, on state transition inside->ouside triggers an event.
function FiguresCollection(canvas, callback)
{
var buffer = [];
var lastHitFigure = null;
var addFigure = function(figure)
{
buffer.push(figure);
}
var onMouseMove = function(e)
{
var currentHit = null;
// iterating from the other end, recently added figures are overlapping previous ones
for (var i= buffer.length-1;i>=0;i--)
{
if (distance(e.offsetX, e.offsetY, buffer[i].x, buffer[i].y) <= buffer[i].radius) {
// the cursor is inside Figure i
// if it come from another figure
if (lastHitFigure !== i)
{
console.log("The cursor had left figure ", lastHitFigure, " and entered ",i);
callback(buffer[i]);
}
lastHitFigure = i;
currentHit = i;
break; // we do not care about figures potentially underneath
}
}
if (lastHitFigure !== null && currentHit == null)
{
console.log("the cursor had left Figure", lastHitFigure, " and is not over any other ");
lastHitFigure = null;
callback(buffer[lastHitFigure]);
}
}
}
canvas.addEventListener("mousemove", onMouseMove);
this.addFigure = addFigure;
}
Now use it:
var col = new FiguresCollection(canvas, c=> console.log("The cursor had left, ", c) );
for(let i in circles)
{
c.addFigure(circles[i]);
}
// I hope I got the code right. I haven't tested it. Please point out any issues or errors.
Related
Still learning Canvas. How can I fade out each tile in an image that I have as Image Data in an array:
for (let i = 0; i < tilesY; i++) {
for (let j = 0; j < tilesX; j++) {
this.ctxTile.putImageData(this.tileData[itr], j * tileWidth, i * tileHeight);
itr += 1
}
}
I understand that the solution must have something to do with compositing? I want to fade out each tile individually. The putImageData works and the image is inside canvas, and assembled as a set of tiles.
Thanks
Usually you'd just play with the ctx.globalAlpha property at the time of drawing to your context to set your tile's alpha. However, putImageData is kind of a strange beast in the API in that it ignores the context transformation, clipping areas and in our case compositing rules, including globalAlpha.
So one hack around would be to "erase" the given tile after we've drawn it. For this we can use the globalCompositeOperation = "destination-out" property that we'll use on a call to a simple fillRect() with the inverse globalAlpha we want. (Luckily putImageData always draws only rectangles).
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const tileWidth = 50;
const tileHeight = 50;
class Tile {
constructor(x, y, width, height) {
this.x = Math.round(x); // putImageData only renders at integer coords
this.y = Math.round(y);
this.width = width;
this.height = height;
this.img = buildImageData(width, height);
this.alpha = 1;
}
isPointInPath(x, y) {
return x >= this.x && x <= this.x + this.width &&
y >= this.y && y <= this.y + this.height;
}
draw() {
ctx.putImageData(this.img, this.x, this.y);
ctx.globalAlpha = 1 - this.alpha; // inverse alpha
// the next drawing will basically erase what it represents
ctx.globalCompositeOperation = "destination-out";
ctx.fillRect(this.x, this.y, this.width, this.height);
// restore the context
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over";
}
}
const tiles = Array.from({ length: 5 }, (_, i) => new Tile(i * tileWidth * 1.25, 0, tileWidth, tileHeight));
canvas.onclick = (e) => {
const x = e.clientX - canvas.offsetLeft;
const y = e.clientY - canvas.offsetTop;
const clickedTile = tiles.find((tile) => tile.isPointInPath(x, y));
if (clickedTile) { clickedTile.alpha -= 0.1 };
redraw();
};
redraw();
function redraw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
tiles.forEach((tile) => tile.draw());
}
function buildImageData(width, height) {
const img = new ImageData(width, height);
const arr = new Uint32Array(img.data.buffer);
for (let i = 0; i < arr.length; i++) {
arr[i] = Math.random() * 0xFFFFFF + 0xFF000000;
}
return img;
}
<p>Click on each tile to lower its alpha.</p>
<canvas></canvas>
However this means that for each tile we have one putImageData + one composited fillRect. If you've got a lot of tiles, that makes for a pretty big overhead.
So instead the best might be to convert all your ImageData objects to ImageBitmap ones. To understand the difference between both I invite you to read this answer of mine.
Once we have ImageBitmaps, we can apply the globalAlpha on our draw call directly:
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const tileWidth = 50;
const tileHeight = 50;
class Tile {
constructor(x, y, width, height) {
this.x = Math.round(x); // putImageData only renders at integer coords
this.y = Math.round(y);
this.width = width;
this.height = height;
this.alpha = 1;
const imgData = buildImageData(width, height);
// createImageBitmap is "async"
this.ready = createImageBitmap(imgData)
.then((bmp) => this.img = bmp);
}
isPointInPath(x, y) {
return x >= this.x && x <= this.x + this.width &&
y >= this.y && y <= this.y + this.height;
}
draw() {
// single draw per tile
ctx.globalAlpha = this.alpha;
ctx.drawImage(this.img, this.x, this.y, this.width, this.height);
ctx.globalAlpha = 1;
}
}
const tiles = Array.from({ length: 5 }, (_, i) => new Tile(i * tileWidth * 1.25, 0, tileWidth, tileHeight));
canvas.onclick = (e) => {
const x = e.clientX - canvas.offsetLeft;
const y = e.clientY - canvas.offsetTop;
const clickedTile = tiles.find((tile) => tile.isPointInPath(x, y));
if (clickedTile) { clickedTile.alpha -= 0.1 };
redraw();
};
// wait for all the ImageBitmaps are generated
Promise.all(tiles.map((tile) => tile.ready)).then(redraw);
function redraw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
tiles.forEach((tile) => tile.draw());
}
function buildImageData(width, height) {
const img = new ImageData(width, height);
const arr = new Uint32Array(img.data.buffer);
for (let i = 0; i < arr.length; i++) {
arr[i] = Math.random() * 0xFFFFFF + 0xFF000000;
}
return img;
}
<p>Click on each tile to lower its alpha.</p>
<canvas></canvas>
I am trying to make a game in which a player has an equippable weapon. As of now, I have set this weapon to be an image of a bow, and I want to have said weapon to move around the player while facing the mouse. This is similar to buildroyale.io, where the player rotates along with his weapon to face the mouse.
As of now, with the help of #Justin, I've got the bow rotating (somewhat) on the screen. It only shows up when the left click is down, as desired, but does not rotate as expected. Here is a clip showcasing how it moves: clip
Here is the code I use:
class EventHandler {
equip_weapon() {
if (holding) {
player.weapon = player.bag.slot_1;
canv.globalAlpha = 1;
player.weapon.equip();
player.weapon.update();
}
}
}
class Weapon {
image_path;
constructor(image_path) {
this.x = player.x + 30;
this.y = player.y + 30;
this.width = 120;
this.height = 120;
this.angle = 0;
this.distance = 50;
this.image = image_path;
}
equip() {
this.angle = Math.atan2(mouse.y - this.y, mouse.x - this.x)
canv.save();
canv.translate(this.x, this.y);
canv.rotate(this.angle);
canv.drawImage(this.image, this.distance, -this.height/2, this.width, this.height);
canv.restore();
}
update() {
this.x = player.x + player.width / 2;
this.y = player.y + player.height / 2;
}
}
bow = new Weapon(bow_image);
player.bag.slot_1 = bow;
aim_bounds = document.documentElement.getBoundingClientRect();
class Player {
constructor() {
this.name = null;
this.speed = 5;
this.skin = player_sheet;
this.can_move = true;
this.is_moving = false;
this.width = 68;
this.height = 68;
this.scale = 1;
this.x = 566;
this.y = 316;
this.direction = facing.down;
this.frame = 0;
this.shadow_offset = 25;
this.frame_rate = 10;
this.health = 100;
this.clip_amount = 10;
this.weapon = null;
// Player inventory
this.bag = {
slot_1 : null,
slot_2 : null,
slot_3 : null,
slot_4 : null,
slot_5 : null,
offhand : null,
armor : null
}
this.is_menu_open = false;
console.log("Player constructed!");
}
update() {
// Animation updates
if (game.tick % this.frame_rate == 0 && this.is_moving && !this.is_menu_open) {
this.frame += 68;
if (this.frame >= 272) { this.frame = 0; }
} else if (!this.is_moving) { this.frame = 0; }
// Movement updates
if (this.can_move) {
if (controller.up) { this.direction = facing.up; this.y -= this.speed; this.is_moving = true; }
else if (controller.down) { this.direction = facing.down; this.y += this.speed; this.is_moving = true; }
else if (controller.left) { this.direction = facing.left; this.x -= this.speed; this.is_moving = true; }
else if (controller.right) { this.direction = facing.right; this.x += this.speed; this.is_moving = true; }
if (!controller.up && !controller.down && !controller.left && !controller.right) { this.is_moving = false; }
}
// Checks
if (this.is_menu_open) { this.can_move = false; } else { this.can_move = true; }
document.getElementById("health_bar").value = this.health;
if (this.is_menu_open) { menu.style.display = "block"; } else { menu.style.display = "none"; }
}
animate() {
// Player shadow
canv.drawImage(player_shadow, this.x, this.y + this.shadow_offset);
// Player
canv.globalAlpha = 1;
canv.drawImage(player_sheet, (sprite.x + this.frame), (sprite.y * this.direction),
sprite.width, sprite.height,
this.x, this.y,
this.width, this.height);
}
}
Here is a download to my current code in case you need more to debug: game.zip
The way I handle this is to make sure my image is first oriented correctly. In my case my bow image would look like this (ignore the quality). Just see that it faces right.
In the draw function I use translate() to position the image and I use the x and y inside the drawImage(img, x, y, w, h) to draw the image centered along the top edge of the canvas. The x (set to 50 in this example) position is essentially the radius of the image rotation and the y is just to center my bow's arrow on the y axis of the canvas.
Using Math.atan2() I can rotate the image
this.angle = Math.atan2(mouse.y - this.y, mouse.x - this.x)
In this snippet I have two lines that are commented out. If you uncomment them you will better see what is happening.
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
canvas.width = 600;
canvas.height = 600;
let mouse = {x: 10, y: 10}
let canvasBounds = canvas.getBoundingClientRect();
canvas.addEventListener('mousemove', e => {
mouse.x = e.x - canvasBounds.x
mouse.y = e.y - canvasBounds.y
})
let bow = new Image();
bow.src = "https://lh3.googleusercontent.com/g5Sr3HmGZgWx07sRQMvgvtxZ-ErhWNT0_asFdhLIlw-EQMTuUq3BV3YY8d5rrIrZBiJ-Uo2l836Qlmr8dmaCi-dcCCqN6veS6xnE8jSrmdtRtZKnmF5FQ5aTxuVBgB28n6ICoxSlpA=w2400";
class Weapon {
constructor() {
this.x = 200;
this.y = 200;
this.w = 60;
this.h = 60;
this.angle = 0
}
draw() {
this.angle = Math.atan2(mouse.y - this.y, mouse.x - this.x)
ctx.save();
ctx.translate(this.x, this.y);
ctx.rotate(this.angle);
//ctx.fillStyle = 'lightgrey';
//ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(bow, 50, -this.h/2, this.w, this.h)
ctx.restore();
}
}
let bowArrow = new Weapon();
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
bowArrow.draw();
requestAnimationFrame(animate)
}
animate();
<canvas id="canvas"></canvas>
UPDATE:
Here is what I changed in your files and it works well on my end.
engine.controller.js change mousemove and add listener for resize. I'm also pretty sure there's a way you can get rid of the getMouse function since the listener below gets mouse coordinates for you.
canvas.addEventListener("mousemove", function (event) {
mouse_position = event
mouse.x = event.x - canvas_bounds.x;
mouse.y = event.y - canvas_bounds.y;
})
window.addEventListener('resize', () => {
canvas_bounds = canvas.getBoundingClientRect();
})
engine.eventhandler.js
class Weapon {
image_path;
constructor(image_path) {
this.x = player.x + player.width/2;
this.y = player.y + player.height/2;
this.w = 60;
this.h = 60;
this.angle = 0;
this.image = image_path;
}
equip() {
this.x = player.x + player.width/2;
this.y = player.y + player.height/2;
this.angle = Math.atan2(mouse.y - this.y, mouse.x - this.x)
canv.save();
canv.translate(this.x, this.y);
canv.rotate(this.angle);
canv.drawImage(this.image, 50, -this.h/2, this.w, this.h);
canv.restore();
}
}
engine.main.js
function setup() {
game = new Game;
player = new Player;
controller = new Controller;
event_handler = new EventHandler;
canvas.width = 1200;
canvas.height = 700;
canvas_bounds = canvas.getBoundingClientRect();
// REMOVE LATER
bow = new Weapon(bow_image);
player.bag.slot_1 = bow;
document.getElementById("bag").style.display = "block";
}
engine.setup.js
// Weapon stuff
var weapon_equiped = false;
var canvas = document.getElementById("canvas");
let mouse = {
x : 10,
y : 10
}
let canvas_bound;
var mouse_position, holding;
var rect, mouse_x, mouse_y;
...and
//you have a space between canvas. height in both fillRect
canv.fillRect(0, 0, canvas.width, canvas. height);
I think that's all I changed. Pretty sure you can use your mouse_x or the mouse.x but you probably don't need both in your code.
Right now, my ball moves very slowly, at 1px on w, a, s, or d click. I don't really want to do a setInterval function. I'm not sure what to do to make my ball move faster, but still at 1px for a second. Alternatively, maybe someone could show me how to make smooth ball movement but still at higher speeds, like 10px? Thanks a ton!
var canvas = document.getElementById("canvas")
var ctx = canvas.getContext('2d')
canvas.width = window.screen.width
canvas.height = window.screen.height
var x = canvas.width / 2;
var y = canvas.height / 2;
r = 50;
var dx = 1;
var dy = -1;
ctx.beginPath();
ctx.arc(x, y, r, 0, 2 * Math.PI);
ctx.stroke();
window.onkeypress = function(e) {
console.log(e.keyCode)
if (e.keyCode == 119) {
function drawCircle() {
ctx.beginPath();
ctx.arc(x, y, r, 0, 2 * Math.PI);
ctx.stroke();
}
ctx.clearRect(0, 0, canvas.width, canvas.height)
window.requestAnimationFrame(drawCircle)
y += dy
} else if (e.keyCode == 97) {
function drawCircle() {
ctx.beginPath();
ctx.arc(x, y, r, 0, 2 * Math.PI);
ctx.stroke();
}
ctx.clearRect(0, 0, canvas.width, canvas.height)
window.requestAnimationFrame(drawCircle)
x -= dx
} else if (e.keyCode == 115) {
function drawCircle() {
ctx.beginPath();
ctx.arc(x, y, r, 0, 2 * Math.PI);
ctx.stroke();
}
ctx.clearRect(0, 0, canvas.width, canvas.height)
window.requestAnimationFrame(drawCircle)
y -= dy
} else if (e.keyCode == 100) {
function drawCircle() {
ctx.beginPath();
ctx.arc(x, y, r, 0, 2 * Math.PI);
ctx.stroke();
}
ctx.clearRect(0, 0, canvas.width, canvas.height)
window.requestAnimationFrame(drawCircle)
x += dx
}
}
window.onload = function() {
}
You need to separate the key event listening from the animation loop. Here's a small OOP based code snippet, which stores the direction of the ball into a lookup table type object. When a key is pressed down, a direction is stored (as -1/1), when a key is released, the direction is marked as 0. The animation loop runs independently from the key events, and moves the ball based on the stored directions and speed property.
// Mathematical constants
const dCorr = 1 / Math.sqrt(2),
PI2 = Math.PI * 2;
class Game {
constructor(cnvId, stKeys) {
// Steering keys
this.keys = stKeys;
const negKeys = Object.keys(stKeys);
this.lKey = negKeys[0];
this.uKey = negKeys[1];
this.rKey = negKeys[2];
this.dKey = negKeys[3];
// Ball parameters
this.x = 100;
this.y = 100;
this.r = 50;
this.speed = 6;
// The game area
this.cnv = document.querySelector(cnvId);
this.ctx = this.cnv.getContext('2d');
// this binding for contextless method calls
this.keyDown = this.keyDown.bind(this);
this.keyUp = this.keyUp.bind(this);
this.animate = this.animate.bind(this);
}
init() {
const cnv = this.cnv;
// Add events
document.addEventListener('keydown', this.keyDown);
document.addEventListener('keyup', this.keyUp);
// Set game area
cnv.width = window.innerWidth - 20;
cnv.height = window.innerHeight - 20;
// Start the animation
this.animate();
return this;
}
keyDown(e) {
const key = e.code;
if (!(this.keys.hasOwnProperty(key))) {return;} // Quit, not a steering key
this.keys[key] = (key === this.lKey || key === this.uKey) ? -1 : 1;
}
keyUp(e) {
const key = e.code;
if (!(this.keys.hasOwnProperty(key))) {return;} // Quit, not a steering key
this.keys[key] = 0;
}
animate() {
window.requestAnimationFrame(this.animate);
// Calculate coordinates delta
let dX = (this.keys[this.lKey] + this.keys[this.rKey]) * this.speed,
dY = (this.keys[this.dKey] + this.keys[this.uKey]) * this.speed;
// Diagonal speed correction
if (dX && dY) {
dX *= dCorr;
dY *= dCorr;
}
// Calculate the coordinates
this.x += dX;
this.y += dY;
// Move the ball
this.ctx.clearRect(0, 0, this.cnv.width, this.cnv.height);
this.moveBall(this.x, this.y);
}
moveBall(x, y) {
this.ctx.beginPath();
this.ctx.arc(x, y, this.r, 0, PI2);
this.ctx.stroke();
}
}
const game = new Game(
'#cnv', // Unique selector for the canvas
{KeyA: 0, KeyW: 0, KeyD: 0, KeyS: 0} // Steering keys, left/up/right/down
).init();
<canvas id="cnv" width="300" height="300"></canvas>
This code doesn't contain border collision detection, but it's easy to add, see an example at jsFiddle. Notice also, that KeyboardEvent.keyCode is deprecated, KeyboardEvent.code is used instead. OOP is not necessary, but it helps to reuse the code, and you can add as many canvases as you need to the same page and move the ball with different keys. The class also works as it is on any page when ever the page has a canvas with an unique property, which can be used as a CSS selector (class, id, data-* attribute etc.).
You code is a complete re-write so I will give you an example using ES6 classes. Classes are great for this type of thing. In the snippet below I have one for the circle and one for the controller. You can create many instances of circle and controllers with just by using classes.
You can also use an animation function to draw/animate all of you objects on screen.
var canvas = document.getElementById("canvas")
var ctx = canvas.getContext('2d')
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
//classes allow you to easily build the same object over and over
class Circle {
constructor(x, y, r) {
this.x = x;
this.y = y;
this.r = r;
this.color = "lightgrey"; //if you wanted to add a fillStyle
this.dx = 0;
this.dy = 0;
}
//draws the object
draw() {
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI);
ctx.stroke();
ctx.closePath();
}
//updates anything related to the object
update() {
//increasingly adds 0.5 to the dy or dx
if (controller1.up) {this.dy -= 0.5};
if (controller1.right) {this.dx += 0.5};
if (controller1.down) {this.dy += 0.5};
if (controller1.left) {this.dx -= 0.5};
//if dy or dx is being added to so will the x and y. If not then they are 0.
this.x += this.dx;
this.y += this.dy;
//Multiplying by a number less then 1 will prevent the object from gaining infinite speed and also cause the object to stop. Can be changed to anything below 1. This will also change how rigidly the circle comes to a stop. it can slide or absuplty stop.
this.dx *= 0.9;
this.dy *= 0.9;
//calling the draw() in here so I don't have to call it in the animate loop. Either way works.
this.draw();
}
}
class Controller {
constructor() {
this.up = false;
this.right = false;
this.down = false;
this.left = false;
let keyEvent = (e) => {
//if the condition is true it will set up/down/left/right to the true or false. e.type will either be 'keyup' or 'keydown'.
if (e.code == "KeyW") {this.up = e.type == 'keydown'};
if (e.code == "KeyD") {this.right = e.type == 'keydown'};
if (e.code == "KeyS") {this.down = e.type == 'keydown'};
if (e.code == "KeyA") {this.left = e.type == 'keydown'};
}
addEventListener('keydown', keyEvent);
addEventListener('keyup', keyEvent);
}
}
//Create instances of you classes
let circle1 = new Circle(canvas.width/2, canvas.height/2, 50, 0, Math.PI*2);
//can add more circles if you want
//let circle2 = new Circle(100, 100, 20, 0, Math.Pi*2);
let controller1 = new Controller();
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
circle1.update();
//can add circle2.draw() if you want the other circle to be staionary.
//circle2.draw();
requestAnimationFrame(animate)
}
animate();
body {
overflow:hidden;
}
<canvas id="canvas"></canvas>
I am trying to detect if two balls are intersecting on a HTML5 canvas.
I need a function called intersect as a part of a constructor object called Ball.
This function will take one Ball object as an argument and return true if the two balls on the canvas are touching/ intersecting. and false otherwise.
I cant figure out how to pass in a new instance of a ball to the intersect function and then compare it to another ball on the canvas.
The function I'm working on the is the final function intersect at the end of the Ball Object.
Please see below for the code i have so far.
Any help would be greatly appreciated.
<!DOCTYPE html>
<hmtl>
<head>
<meta charset="UTF-8">
<title>Canvas</title>
<style type="text/css">
canvas{
border: 1px solid black;
}
</style>
</head>
<body>
<canvas id="canvasOne" ></canvas>
<script type="text/javascript">
// Gets a handle to the element with id canvasOne.
var canvas = document.getElementById("canvasOne");
// Set the canvas up for drawing in 2D.
var ctx = canvas.getContext("2d");
canvas.width = 500;
canvas.height = 500;
function Ball(xpos,ypos,r) {
this.xpos = xpos;
this.ypos = ypos;
this.r = r;
this.move = function(addx,addy){
this.xpos = this.xpos + addx;
this.ypos = this.ypos + addy;
};
this.resize = function(setr){
this.r = setr;
};
this.draw = function(){
for (var i = 0; i < 7; i++) {
ctx.beginPath();
ctx.moveTo(ball.xpos, ball.ypos);
ctx.arc(ball.xpos, ball.ypos, ball.r, i*(2 * Math.PI / 7), (i+1)*(2 * Math.PI / 7));
ctx.lineWidth = 2;
ctx.strokeStyle = '#444';
ctx.stroke();
}
ctx.beginPath();
ctx.moveTo(ball.xpos, ball.ypos);
ctx.arc(ball.xpos,ball.ypos,ball.r-10,0,2*Math.PI);
ctx.lineWidth = 2;
ctx.strokeStyle = '#444';
ctx.stroke();
};
this.rotate = function(){
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Move registration point to the center of the canvas
ctx.translate(ball.xpos, ball.ypos);
// Rotate 1 degree
ctx.rotate(Math.PI / 180);
// Move registration point back to the top left corner of canvas
ctx.translate(-ball.xpos, -ball.ypos);
ball.draw();
ctx.restore();
};
this.contains = function(x, y){
this.x = this.x;
this.y = this.y;
if(Math.sqrt((x-ball.xpos)*(x-ball.xpos) + (y-ball.ypos)*(y-ball.ypos)) <= ball.r)
{
return true;
}else{
return false;
}
};
this.intersect = function(){
this.ball1 = this.ball1;
var distance = (ball.xpos * ball.xpos) + (ball.ypos *ball.ypos);
if(distance <= (ball.r + ball.r)*(ball.r + ball.r)){
return true;
}else{
return false;
}
};
}
var ball = new Ball(100,100,100);
ball.draw();
</script>
</body>
</html>
First off, if you aren't going to use the this keyword in your class, then why make it a class?
You can setup your intersect to take a Ball as a parameter. From here you can calculate the collision between this and the parameter Ball.
You distance function was off, as it only looked on the this object, and i fixed the this problem in your code:
var canvas = document.body.appendChild(document.createElement("canvas"));
// Set the canvas up for drawing in 2D.
var ctx = canvas.getContext("2d");
canvas.width = 500;
canvas.height = 500;
function Ball(xpos, ypos, r) {
this.xpos = xpos;
this.ypos = ypos;
this.r = r;
this.move = function(addx, addy) {
this.xpos = this.xpos + addx;
this.ypos = this.ypos + addy;
};
this.resize = function(setr) {
this.r = setr;
};
this.draw = function() {
for (var i = 0; i < 7; i++) {
ctx.beginPath();
ctx.moveTo(this.xpos, this.ypos);
ctx.arc(this.xpos, this.ypos, this.r, i * (2 * Math.PI / 7), (i + 1) * (2 * Math.PI / 7));
ctx.lineWidth = 2;
ctx.stroke();
}
ctx.beginPath();
ctx.moveTo(this.xpos, this.ypos);
ctx.arc(this.xpos, this.ypos, this.r - 10, 0, 2 * Math.PI);
ctx.lineWidth = 2;
ctx.stroke();
};
this.rotate = function() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Move registration point to the center of the canvas
ctx.translate(this.xpos, this.ypos);
// Rotate 1 degree
ctx.rotate(Math.PI / 180);
// Move registration point back to the top left corner of canvas
ctx.translate(-this.xpos, -this.ypos);
this.draw();
ctx.restore();
};
this.contains = function(x, y) {
this.x = this.x;
this.y = this.y;
if (Math.sqrt((x - this.xpos) * (x - this.xpos) + (y - this.ypos) * (y - this.ypos)) <= this.r) {
return true;
} else {
return false;
}
};
//put "ball" as a paremeter
//ball will be the foreign Ball to test intersection against
this.intersect = function(ball) {
var productX = this.xpos - ball.xpos;
var productY = this.ypos - ball.ypos;
var distance = Math.sqrt(productX * productX + productY * productY);
if (distance <= (this.r + ball.r)) {
return true;
} else {
return false;
}
};
}
var ball1 = new Ball(100, 100, 100);
var ball2 = new Ball(240, 140, 40);
function update(evt) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (evt !== void 0) {
ball2.xpos = evt.offsetX;
ball2.ypos = evt.offsetY;
}
//Pass the ball as an argument to the method
ctx.strokeStyle = ball1.intersect(ball2) ? "red" : '#444';
ball1.draw();
ball2.draw();
}
update();
canvas.onmousemove = update;
I cant figure out how to pass in a new instance of a ball to the
intersect function
Well to pass anything really it should have an argument.
this.intersect = function(otherball){
// then compare the two ball objects
Then...
var ball1 = new Ball(100,100,100);
var ball2 = new Ball(100,100,100);
ball1.draw();
ball2.draw();
console.log(ball1.intersect(ball2));
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();