Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to detect when mouse is outside of a certain circle?

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>
like image 469
AttackTheWar Avatar asked Feb 23 '20 21:02

AttackTheWar


2 Answers

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>
like image 126
Naor Tedgi Avatar answered Sep 28 '22 23:09

Naor Tedgi


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>
like image 36
Blindman67 Avatar answered Sep 28 '22 21:09

Blindman67