Related
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.
I'm currently developing a small game for my capstone project. In the game, the user tries to avoid rectangles of random sizes the move from the right side of the screen to the left at a set speed.
It's built using object-oriented Javascript, and I've assigned it an anonymous function, however, I can't seem to get it to generate a shape and animate it more than the initial time the function is called. The problem can be solved if I create more than one object, but I would like this function to run automatically and generate more than just the first rectangle.
I've tried to call the function with an interval to force it to re-run the function with no results. I also attempted to separate the initialization function to call it with a parameter to generate the number of shapes given to it.
This is the function that generates the shape with the initial call, and determines the color, size, and location as well as draws it on the canvas.
var randomRectangle = function(){
this.init = function() {
this.speed = 4;
this.x = canvas.width-50;
this.y = Math.floor(Math.random()*280) + 40;
this.w = Math.floor(Math.random()*200) + 50;
this.h = Math.floor(Math.random()*150) + 20;
this.col = "#b5e61d";
}
this.move = function(){
this.x -= this.speed;
}
this.draw = function(num){
draw.rectangles(this.x, this.y, this.w, this.h, this.col);
}
};
This is where the object is initialized and the loop generates all objects and animations on the canvas.
randRecs = new randomRectangle();
randRecs.init();
function loop(){
draw.clear();
player.draw();
player.move();
wall1.draw();
wall2.draw();
randRecs.draw();
randRecs.move();
}
var handle = setInterval(loop, 30);
I expected the rectangle to continuously be generated at a new y-coordinate with a new size, then move from the right side of the screen to the left. However, only one rectangle is created and animated.
var list = [];
var canvas = document.querySelector('canvas');
var ctx = canvas.getContext('2d');
var randomRectangle = function() {
this.init = function() {
this.speed = 4;
this.x = canvas.width - 50;
this.y = Math.floor(Math.random() * 280) + 40;
this.w = Math.floor(Math.random() * 200) + 50;
this.h = Math.floor(Math.random() * 150) + 20;
this.col = "#b5e61d";
}
this.move = function() {
this.x -= this.speed;
// restart x position to reuse rectangles
// you can change the y value here to a new random value
// or you can just remove with array.splice
if (this.x < -50) this.x = canvas.width - 50;
}
this.draw = function(num) {
draw.rectangles(this.x, this.y, this.w, this.h, this.col);
}
};
function loop() {
draw.clear();
//player.draw();
//player.move();
//wall1.draw();
//wall2.draw();
// call the methods draw and move for each rectangle on the list
for (var i=0; i<list.length; i++) {
rec = list[i];
rec.draw();
rec.move();
}
}
// spawn any number of new rects in a specific interval
var rectsPerSpawn = 1;
function addRects() {
for (var i=0; i<rectsPerSpawn; i++) {
if (list.length < 100) {
var rec = new randomRectangle();
list.push(rec);
rec.init();
}
}
}
// every half second will spawn a new rect
var spawn = setInterval(addRects, 500);
var draw = {
clear: function () {
ctx.clearRect(0,0,canvas.width,canvas.height);
},
rectangles: function (x, y, w, h, col) {
ctx.fillStyle = col;
ctx.fillRect(x,y,w,h);
}
}
var handle = setInterval(loop, 30);
<canvas></canvas>
Please tell me why, when I transfer coordinates options to class Circle, he does not see them?
I want to implement logic when the circle encounters any edge of the screen, it jumps back (change the value of the corresponding vector coordinate to the opposite value by the sign and call the move() method with the new vector)
A little bit about the logic of my code:
I implemented a Circle class with properties:
x - the initial value of the x coordinate
y - the initial value of the y coordinate
diameter - values of width and height
color - fill color
Method: draw() - draws on the screen an element that is described by the specified properties.
Method: move({x = 0, y = 0}) - moves the drawn object along the vector (x, y) - every time period (100ms) changes (adds \ subtracts) the coordinates by the value of x and y in accordance
And the internal method update(), which changes the position of the drawn circle with the corresponding values of color, x, y of the object.
class Circle {
constructor(options) {
this.x = options.x;
this.y = options.y;
this.diameter = options.diameter;
this.color = options.color;
// the circle's move/update animation interval in ms
}
draw() {
const div = document.createElement('div');
div.style.position = 'absolute';
div.style.left = `${this.x}px`;
div.style.top = `${this.y}px`;
div.style.width = `${this.diameter}px`;
div.style.height = `${this.diameter}px`;
div.style.border = "1px solid;";
div.style.borderRadius = "50%";
div.style.backgroundColor = `${this.color}`;
document.body.appendChild(div);
// store the reference to the div element for later use
this.circle = div;
// use the refacterd positioning function
this._reposition();
}
setColor(newColor) {
return this.color = newColor;
}
move({x = 0, y = 0, duration = 1000}) {
this.updateInterval = 100;
this.direction = 1;
this.destX = 0,
this.destY = 4;
// store the current time in ms
this.startTime = Date.now();
this.duration = duration;
// if a previous setInterval of this circle instance
// is still running, clear it (stop it)
clearInterval(this.intervalId);
// start the update
window.requestAnimationFrame(this._update.bind(this));
}
_update() {
// set the x and y coordinates according to the progress
let newX = this.x + this.direction * this.destX;
let newY = this.y + this.direction * this.destY;
if (newY >= window.innerHeight - this.diameter) {
this.direction = -1;
} else if (newY <= 0) {
this.direction = 1;
}
if (newX >= window.innerWidth - this.diameter) {
this.direction = -1;
} else if (newX <= 0) {
this.direction = 1;
}
this.x = newX;
this.y = newY;
this._reposition();
window.requestAnimationFrame(this._update.bind(this));
}
_reposition() {
// set the position of the circle instance
this.circle.style.left = `${this.x}px`;
this.circle.style.top = `${this.y}px`;
}
}
const options = {
x: 100,
y: 100,
diameter: 100,
color: 'red'
};
const circle = new Circle(options);
circle.draw();
circle.setColor("green");
circle.move({x: 1235, y:0});
Your _update method never resets the color, try
this.circle.style.backgroundColor = `${this.color}`;
inside your _update call :)
I have a card class:
function Card() {
this.image = new Image();
this.x = 0;
this.y = 400;
this.initialX = 0;
this.initialY = 0;
this.scaleFactor = 4;
this.setImage = function(ii){
this.image.src = ii;
};
this.getWidth = function(){
if (this.image == null){
return 0;
}
return this.image.width / this.scaleFactor;
}
this.getHeight = function(){
if (this.image == null){
return 0;
}
return this.image.height / this.scaleFactor;
}
this.pointIsInCard = function(mx, my){
if (mx >= this.x && mx <= (this.x + this.getWidth()) && my >= this.y && my <= (this.y + this.getHeight()))
{
return true;
}
else{
return false;
}
};
};
I then have a deck class:
function Deck(x, y, w, h){
this.x = x;
this.y = y;
this.width = w;
this.height = h;
this.cards = [];
}
I need to add a method in Deck class similar to pointIsInCard instead it will be called pointIsInDeck. The logic will be same i.e to check whether the passed in point falls in the boundary of the object. So seeing this duplication of code I wanted to know what is a good design practice to avoid this duplication? One option I thought of was to extract the method out and create a function for generic object with x, y, width, height but again from OOP principles I thought this method should belong to the class/object. I appreciate any help! Thanks!
A common approach for what you're doing is to attach a Rectangle or similar instance with that functionality to both of your objects, that is:
class Rectangle {
constructor(x, y, width, height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
containsPoint(x, y) {
return x >= this.x && x =< this.width &&
y >= this.y && y =< this.height;
}
}
Then just add it to Card and Deck:
function Card() {
this.rect = new Rectangle(/* Your card shape */);
// ...
}
function Deck() {
this.rect = new Rectangle(/* Your deck shape */);
// ...
}
And you can do:
card.rect.containsPoint();
deck.rect.containsPoint();
If these are classes related to drawing, they would both inherit from something like Rectangle, which they would both inherit this behaviour from.
If they are gameplay-related, I would prefer them each referencing a Rectangle (or its subclass) that they would delegate all UI-related tasks to; then reduce this to the previous paragraph's solution.
You can use Function.prototype.call() to set this at a function call
function Card() {
this.x = 1; this.y = 2;
};
function Deck() {
this.x = 10; this.y = 20;
}
function points(x, y) {
// do stuff
console.log(x, this.x, y, this.y); // `this`: `c` or `d`
}
var c = new Card();
var d = new Deck();
points.call(c, 3, 4); // `this`: `c` within `points` call
points.call(d, 100, 200); // `this`: `d` within `points` call
My question is, what is the best way to tint an image that is drawn using the drawImage method. The target useage for this is advanced 2d particle-effects (game development) where particles change colors over time etc. I am not asking how to tint the whole canvas, only the current image i am about to draw.
I have concluded that the globalAlpha parameter affects the current image that is drawn.
//works with drawImage()
canvas2d.globalAlpha = 0.5;
But how do i tint each image with an arbitrary color value ? It would be awesome if there was some kind of globalFillStyle or globalColor or that kind of thing...
EDIT:
Here is a screenshot of the application i am working with:
http://twitpic.com/1j2aeg/full
alt text http://web20.twitpic.com/img/92485672-1d59e2f85d099210d4dafb5211bf770f.4bd804ef-scaled.png
You have compositing operations, and one of them is destination-atop. If you composite an image onto a solid color with the 'context.globalCompositeOperation = "destination-atop"', it will have the alpha of the foreground image, and the color of the background image. I used this to make a fully tinted copy of an image, and then drew that fully tinted copy on top of the original at an opacity equal to the amount that I want to tint.
Here is the full code:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<title>HTML5 Canvas Test</title>
<script type="text/javascript">
var x; //drawing context
var width;
var height;
var fg;
var buffer
window.onload = function() {
var drawingCanvas = document.getElementById('myDrawing');
// Check the element is in the DOM and the browser supports canvas
if(drawingCanvas && drawingCanvas.getContext) {
// Initaliase a 2-dimensional drawing context
x = drawingCanvas.getContext('2d');
width = x.canvas.width;
height = x.canvas.height;
// grey box grid for transparency testing
x.fillStyle = '#666666';
x.fillRect(0,0,width,height);
x.fillStyle = '#AAAAAA';
var i,j;
for (i=0; i<100; i++){
for (j=0; j<100; j++){
if ((i+j)%2==0){
x.fillRect(20*i,20*j,20,20);
}
}
}
fg = new Image();
fg.src = 'http://uncc.ath.cx/LayerCake/images/16/3.png';
// create offscreen buffer,
buffer = document.createElement('canvas');
buffer.width = fg.width;
buffer.height = fg.height;
bx = buffer.getContext('2d');
// fill offscreen buffer with the tint color
bx.fillStyle = '#FF0000'
bx.fillRect(0,0,buffer.width,buffer.height);
// destination atop makes a result with an alpha channel identical to fg, but with all pixels retaining their original color *as far as I can tell*
bx.globalCompositeOperation = "destination-atop";
bx.drawImage(fg,0,0);
// to tint the image, draw it first
x.drawImage(fg,0,0);
//then set the global alpha to the amound that you want to tint it, and draw the buffer directly on top of it.
x.globalAlpha = 0.5;
x.drawImage(buffer,0,0);
}
}
</script>
</head>
</body>
<canvas id="myDrawing" width="770" height="400">
<p>Your browser doesn't support canvas.</p>
</canvas>
</body>
</html>
There is a method here you can use to tint images, and it's more accurate then drawing coloured rectangles and faster then working on a pixel-by-pixel basis. A full explanation is in that blog post, including the JS code, but here is a summary of how it works.
First you go through the image you are tinting pixel by pixel, reading out the data and splitting each pixel up into 4 separate components: red, green, blue and black. You write each component to a separate canvas. So now you have 4 (red, green, blue and black) versions of the original image.
When you want to draw a tinted image, you create (or find) an off-screen canvas and draw these components to it. The black is drawn first, and then you need set the globalCompositeOperation of the canvas to 'lighter' so the next components are added to the canvas. The black is also non-transparent.
The next three components are drawn (the red, blue and green images), but their alpha value is based on how much their component makes up the drawing colour. So if the colour is white, then all three are drawn with 1 alpha. If the colour is green, then only the green image is drawn and the other two are skipped. If the colour is orange then you have full alpha on the red, draw green partially transparent and skip the blue.
Now you have a tinted version of your image rendered onto the spare canvas, and you just draw it to where ever you need it on your canvas.
Again the code to do this is in the blog post.
Unfortunately, there is not simply one value to change similar to openGL or DirectX libraries I've used in the past. However, it's not too much work to create a new buffer canvas and use the available globalCompositeOperation when drawing an image.
// Create a buffer element to draw based on the Image img
const buffer = document.createElement('canvas');
buffer.width = img.width;
buffer.height = img.height;
const btx = buffer.getContext('2d');
// First draw your image to the buffer
btx.drawImage(img, 0, 0);
// Now we'll multiply a rectangle of your chosen color
btx.fillStyle = '#FF7700';
btx.globalCompositeOperation = 'multiply';
btx.fillRect(0, 0, buffer.width, buffer.height);
// Finally, fix masking issues you'll probably incur and optional globalAlpha
btx.globalAlpha = 0.5;
btx.globalCompositeOperation = 'destination-in';
btx.drawImage(img, 0, 0);
You can now use buffer as your first parameter canvas2d.drawImage. Using multiply you'll get literal tint but hue and color may also be to your liking. Also, this is fast enough to wrap in a function for reuse.
When I created a particle test I just cached images based on rotation (like 35 rotations), color tint, and alpha and created a wrapper so that they were created automatically. Worked well. Yes there should be some kind of tint operation, but when dealing with software rendering your best bet much like in flash is to cache everything.
Particle Example I made for fun
<!DOCTYPE HTML>
<html lang="en">
<head>
<title>Particle Test</title>
<script language="javascript" src="../Vector.js"></script>
<script type="text/javascript">
function Particle(x, y)
{
this.position = new Vector(x, y);
this.velocity = new Vector(0.0, 0.0);
this.force = new Vector(0.0, 0.0);
this.mass = 1;
this.alpha = 0;
}
// Canvas
var canvas = null;
var context2D = null;
// Blue Particle Texture
var blueParticleTexture = new Image();
var blueParticleTextureLoaded = false;
var blueParticleTextureAlpha = new Array();
var mousePosition = new Vector();
var mouseDownPosition = new Vector();
// Particles
var particles = new Array();
var center = new Vector(250, 250);
var imageData;
function Initialize()
{
canvas = document.getElementById('canvas');
context2D = canvas.getContext('2d');
for (var createEntity = 0; createEntity < 150; ++createEntity)
{
var randomAngle = Math.random() * Math.PI * 2;
var particle = new Particle(Math.cos(randomAngle) * 250 + 250, Math.sin(randomAngle) * 250 + 250);
particle.velocity = center.Subtract(particle.position).Normal().Normalize().Multiply(Math.random() * 5 + 2);
particle.mass = Math.random() * 3 + 0.5;
particles.push(particle);
}
blueParticleTexture.onload = function()
{
context2D.drawImage(blueParticleTexture, 0, 0);
imageData = context2D.getImageData(0, 0, 5, 5);
var imageDataPixels = imageData.data;
for (var i = 0; i <= 255; ++i)
{
var newImageData = context2D.createImageData(5, 5);
var pixels = newImageData.data;
for (var j = 0, n = pixels.length; j < n; j += 4)
{
pixels[j] = imageDataPixels[j];
pixels[j + 1] = imageDataPixels[j + 1];
pixels[j + 2] = imageDataPixels[j + 2];
pixels[j + 3] = Math.floor(imageDataPixels[j + 3] * i / 255);
}
blueParticleTextureAlpha.push(newImageData);
}
blueParticleTextureLoaded = true;
}
blueParticleTexture.src = 'blueparticle.png';
setInterval(Update, 50);
}
function Update()
{
// Clear the screen
context2D.clearRect(0, 0, canvas.width, canvas.height);
for (var i = 0; i < particles.length; ++i)
{
var particle = particles[i];
var v = center.Subtract(particle.position).Normalize().Multiply(0.5);
particle.force = v;
particle.velocity.ThisAdd(particle.force.Divide(particle.mass));
particle.velocity.ThisMultiply(0.98);
particle.position.ThisAdd(particle.velocity);
particle.force = new Vector();
//if (particle.alpha + 5 < 255) particle.alpha += 5;
if (particle.position.Subtract(center).LengthSquared() < 20 * 20)
{
var randomAngle = Math.random() * Math.PI * 2;
particle.position = new Vector(Math.cos(randomAngle) * 250 + 250, Math.sin(randomAngle) * 250 + 250);
particle.velocity = center.Subtract(particle.position).Normal().Normalize().Multiply(Math.random() * 5 + 2);
//particle.alpha = 0;
}
}
if (blueParticleTextureLoaded)
{
for (var i = 0; i < particles.length; ++i)
{
var particle = particles[i];
var intensity = Math.min(1, Math.max(0, 1 - Math.abs(particle.position.Subtract(center).Length() - 125) / 125));
context2D.putImageData(blueParticleTextureAlpha[Math.floor(intensity * 255)], particle.position.X - 2.5, particle.position.Y - 2.5, 0, 0, blueParticleTexture.width, blueParticleTexture.height);
//context2D.drawImage(blueParticleTexture, particle.position.X - 2.5, particle.position.Y - 2.5);
}
}
}
</script>
<body onload="Initialize()" style="background-color:black">
<canvas id="canvas" width="500" height="500" style="border:2px solid gray;"/>
<h1>Canvas is not supported in this browser.</h1>
</canvas>
<p>No directions</p>
</body>
</html>
where vector.js is just a naive vector object:
// Vector class
// TODO: EXamples
// v0 = v1 * 100 + v3 * 200;
// v0 = v1.MultiplY(100).Add(v2.MultiplY(200));
// TODO: In the future maYbe implement:
// VectorEval("%1 = %2 * %3 + %4 * %5", v0, v1, 100, v2, 200);
function Vector(X, Y)
{
/*
this.__defineGetter__("X", function() { return this.X; });
this.__defineSetter__("X", function(value) { this.X = value });
this.__defineGetter__("Y", function() { return this.Y; });
this.__defineSetter__("Y", function(value) { this.Y = value });
*/
this.Add = function(v)
{
return new Vector(this.X + v.X, this.Y + v.Y);
}
this.Subtract = function(v)
{
return new Vector(this.X - v.X, this.Y - v.Y);
}
this.Multiply = function(s)
{
return new Vector(this.X * s, this.Y * s);
}
this.Divide = function(s)
{
return new Vector(this.X / s, this.Y / s);
}
this.ThisAdd = function(v)
{
this.X += v.X;
this.Y += v.Y;
return this;
}
this.ThisSubtract = function(v)
{
this.X -= v.X;
this.Y -= v.Y;
return this;
}
this.ThisMultiply = function(s)
{
this.X *= s;
this.Y *= s;
return this;
}
this.ThisDivide = function(s)
{
this.X /= s;
this.Y /= s;
return this;
}
this.Length = function()
{
return Math.sqrt(this.X * this.X + this.Y * this.Y);
}
this.LengthSquared = function()
{
return this.X * this.X + this.Y * this.Y;
}
this.Normal = function()
{
return new Vector(-this.Y, this.X);
}
this.ThisNormal = function()
{
var X = this.X;
this.X = -this.Y
this.Y = X;
return this;
}
this.Normalize = function()
{
var length = this.Length();
if(length != 0)
{
return new Vector(this.X / length, this.Y / length);
}
}
this.ThisNormalize = function()
{
var length = this.Length();
if (length != 0)
{
this.X /= length;
this.Y /= length;
}
return this;
}
this.Negate = function()
{
return new Vector(-this.X, -this.Y);
}
this.ThisNegate = function()
{
this.X = -this.X;
this.Y = -this.Y;
return this;
}
this.Compare = function(v)
{
return Math.abs(this.X - v.X) < 0.0001 && Math.abs(this.Y - v.Y) < 0.0001;
}
this.Dot = function(v)
{
return this.X * v.X + this.Y * v.Y;
}
this.Cross = function(v)
{
return this.X * v.Y - this.Y * v.X;
}
this.Projection = function(v)
{
return this.MultiplY(v, (this.X * v.X + this.Y * v.Y) / (v.X * v.X + v.Y * v.Y));
}
this.ThisProjection = function(v)
{
var temp = (this.X * v.X + this.Y * v.Y) / (v.X * v.X + v.Y * v.Y);
this.X = v.X * temp;
this.Y = v.Y * temp;
return this;
}
// If X and Y aren't supplied, default them to zero
if (X == undefined) this.X = 0; else this.X = X;
if (Y == undefined) this.Y = 0; else this.Y = Y;
}
/*
Object.definePropertY(Vector, "X", {get : function(){ return X; },
set : function(value){ X = value; },
enumerable : true,
configurable : true});
Object.definePropertY(Vector, "Y", {get : function(){ return X; },
set : function(value){ X = value; },
enumerable : true,
configurable : true});
*/
This question still stands. The solution some seem to be suggesting is drawing the image to be tinted onto another canvas and from there grabbing the ImageData object to be able to modify it pixel by pixel, the problem with this is that it is not really acceptable in a game development context because i basically will have to draw each particle 2 times instead of 1. A solution i am about to try is to draw each particle once on a canvas and grabbing the ImageData object, before the actual application starts, and then work with the ImageData object instead of the actual Image object but it might prove kind of costly to create new copies since i will have to keep an unmodified original ImageData object for each graphic.
I would take a look at this: http://www.arahaya.com/canvasscript3/examples/ he seems to have a ColorTransform method, I believe he is drawing a shape to do the transform but perhaps based on this you can find a way to adjust a specific image.