¿Cómo detectar cuando el mouse está fuera de cierto círculo?

8

Cuando un mouse se mueve sobre una imagen. Se detecta por esta declaración if:

if ((distance(circles[this.index].x, circles[this.index].y, mouse.x, mouse.y)) < circles[this.index].radius)

También quiero detectar cuando un mouse está fuera de una imagen. Después de esa declaración if anterior que no puedo usar, la razón es porque:

Cuando genero varias imágenes en la pantalla y cuando mi mouse se mueve sobre 1 imagen. Se desplaza sobre esa imagen y el código la detecta, pero tampoco sobre todas las demás imágenes. Esa es la razón por la que se muestra 4 veces "círculo exterior" y 1 vez "círculo interior"

Como se ve en el registro:

Salida de Console.log:

Mouse inside circle 
Mouse outside circle 4 
Mouse inside circle 
Mouse outside circle 4 

Estoy buscando una forma de detectar cuándo el mouse está saliendo de un círculo.

Puede encontrar el código con el que estoy trabajando a continuación:

PD: es importante que detecte en qué círculo (índice) está el mouse y se va. Quiero crear una gran cantidad de imágenes, pero en el código a continuación utilicé 5 para los propósitos de demostración.

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>

AttackTheWar
fuente

Respuestas:

3

simplemente agregue otra propiedad al círculo

  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
    }

y luego la lógica de actualización cambia a esto

 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}`)
        }

compruebe si los círculos se superponen y puede decidir si desea actualizar

  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>

Naor Tedgi
fuente
Hola gracias por la respuesta Al pasar el cursor sobre varios círculos en 1 lugar (al mismo tiempo). La propiedad del mouseinside de varios círculos se establece en "verdadero". ¿Cómo puedo priorizar (por índice) solo la propiedad "mouseinside" de 1 círculo para que se establezca en "true".
AttackTheWar
¿Cómo pasa exactamente más de un círculo al mismo tiempo?
Naor Tedgi
@NaorTedgi El OP probablemente significó cuando hay dos círculos superpuestos.
Richard
Cuando 2 círculos se superponen @NaorTedgi
AttackTheWar
Agrego un fragmento sobre cómo reconocer si los círculos se superponen t, luego puedes decidir si deseas actualizar ese círculo en particular
Naor Tedgi
1

Ambigüedades

No está claro lo que necesita con respecto a los círculos y algún punto (en este punto de respuesta es un sustituto del mouse y solo requiere que tenga las propiedades xy ysea ​​válido).

La falta de información en su pregunta se refiere a los hechos.

  • que muchos círculos pueden estar debajo del punto al mismo tiempo.

  • y que más de un círculo puede moverse de abajo hacia afuera o hacia afuera debajo del punto por cuadro.

  • La redacción de la pregunta sugiere que está detrás de un solo círculo que está en conflicto con las dos preocupaciones anteriores.

Supuestos

Asumiré que la interacción con los círculos es más que un simple evento bajo como interacción. Que pueden incluir comportamientos relacionados con la animación que se desencadenan por el estado relacionado con el punto.

Supongo que el orden visual de los círculos determinará cómo seleccionar círculos de interés.

Que todos los círculos por cuadro cumplan las condiciones requeridas y se pueda acceder rápidamente.

Esa actuación es importante ya que deseas tener muchos círculos que interactúen con un punto.

Que solo hay un punto (mouse, touch, otra fuente) por cuadro que interactúa con los círculos

No hay requisitos para la interacción círculo-círculo.

Solución

El siguiente ejemplo cubre los supuestos anteriores y resuelve cualquier ambigüedad en la pregunta. Está diseñado para ser eficiente y flexible.

Los círculos se almacenan en una matriz que tiene sus propiedades extendidas llamadas circles

Renderizado y conjuntos de estados

La función circles.updateDraw(point)actualiza y dibuja todos los círculos. El argumento pointes un punto para verificar el círculo. Por defecto es elmouse .

Todos los círculos se dibujan con un contorno. Los círculos debajo del punto (p. Ej., El mouse) se llenan de verde, los círculos que acaban de moverse debajo del punto (p. Ej., OnMouseOver) se llenan de amarillo, los círculos que acaban de salir de debajo se llenan de rojo.

Hay 3 matrices como propiedades de círculos que contienen círculos como se define ...

  • circles.under Todos los círculos debajo del punto
  • circles.outFromUnder Todos los círculos salen por debajo del punto
  • circles.newUnder Todos los círculos nuevos debajo del punto

Estas matrices están pobladas por la función circles.updateDraw(point)

Consulta todos los círculos del estado del punto

Los círculos también tienen 3 funciones que se refieren a las matrices anteriores como setel conjunto predeterminado circles.under.

Las funciones son ..

  • circles.firstInSet(set)Devuelve el primer círculo (La parte inferior visual más) en setoundefined
  • circles.lastInSet(set)Devuelve el último círculo (La parte superior visual más) en setoundefined
  • circles.closestInSet(set)Devuelve el círculo más cercano al punto en setoundefined

Por ejemplo, para obtener el círculo visual superior de la parte superior justo debajo del mouse al que llamaría circles.lastInSet(circles.newUnder)o para obtener el círculo más cercano al mouse de todos los círculos debajo del mouse al que llamaría circles.closestInSet(circles.newUnder)(o como el valor predeterminado es establecer la underllamada circles.closestInSet())

Circula estados adicionales

Cada círculo tiene algunas propiedades adicionales.

  • Circle.distSqr es el cuadrado de la distancia desde el punto
  • Circle.rSqr es el cuadrado del radio calculado cuando se construye.
  • Circle.underCount Este valor se puede usar para aplicar animaciones al círculo en función de su estado relativo al punto.
    • Si positivo es el número de cuadros más 1, el círculo está debajo del punto.
    • Si este valor es 1, entonces el círculo se mueve de abajo hacia abajo.
    • Si este valor es 0, acaba de salir de debajo del punto.
    • Si es negativo, este valor es el número de cuadros que el círculo no está debajo del punto

Ejecución de demostración

Usa el mouse para moverte sobre los círculos. El círculo más cercano y debajo del mouse está lleno de blanco con alfa = 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>

Ciego67
fuente
0

Bueno, el mouse se está moviendo y simplemente puede crear un Conjunto que contendrá objetos circulares que almacenarán los círculos en los que se encuentra:

let circleOfTrust = new Set(); 
//At the initialization you need to add any circles your point is currently in

y luego en el bucle:

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
}

y el 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;

},
Lajos Arpad
fuente
0

Yo propondría lo siguiente:

  1. Mantenga una pila de figuras con el orden de cómo se crearon (o cualquier otro orden significativo). Esto es necesario para detectar movimientos sobre figuras superpuestas.

  2. Implemente una función / método que itere la pila y determine si el cursor está dentro de alguna de las figuras.

  3. Recuerde el último estado, en la transición de estado dentro-> ouside desencadena un evento.

    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;
    }

Ahora úsalo:

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.
Eriks Klotins
fuente