MouseOver function

Can mouseOver function detect if the mouse is on the shape drawn on the canvas. in the reference’s example, it seems it can only detect whether the mouse has entered the element, what about shapes. if it can, how to use it?

There is are no statements in p5js to do this directly you have to create your own code.

Here is a simple example for you to try. The code is below.

let ball = { x: 100, y: 200, r: 50 };
let oblong = { x: 225, y: 20, w: 50, h: 260 };

function setup() {
  createCanvas(320, 320);
  cursor(CROSS);
}

function draw() {
  background(20);
  stroke(255);
  strokeWeight(3);
  if (isOverCircle(ball)) 
    fill(255, 128, 128);
  else 
    fill(64, 255, 64);
  ellipse(ball.x, ball.y, 2 * ball.r, 2 * ball.r);

  if (isOverRectangle(oblong)) 
    fill(255, 128, 128);
  else 
    fill(64, 255, 64);
  rect(oblong.x, oblong.y, oblong.w, oblong.h);
}

function isOverCircle(c, px = mouseX, py = mouseY) {
  let dx = px - c.x, dy = py - c.y;
  return dx * dx + dy * dy <= c.r * c.r;
}

function isOverRectangle(o, px = mouseX, py = mouseY) {
  let dx = px - o.x, dy = py - o.y;
  return dx >= 0 && dy >= 0 && dx < o.w && dy < o.h;
}
1 Like

Hi! In addition to what quark said (which will be at the heart of most good solutions), I’ll add a few possibilities that can work in some limited circumstances:

  1. A quick, limited workaround / cheat:
    If all your shapes are distinctly coloured and do not overlap, you can calculate what shape the mouse is over based on the colour of the pixel the mouse is over.
    To find the colour of a pixel under the mouse, you would use get(mouseX, mouseY). See docs for get
    As you can imagine, this approach is pretty limiting.

  2. For simple prototype games it’s sometimes enough to assume all of your objects are circular, and just check that the mouse is sufficiently near the centre of the imagined circle by comparing the distance between those two points against the radius of the circle. You can use the dist function or quark’s isOverCircle().

  3. Ben Moren’s p5.collide2D addon library provides a number of functions for detecting whether a given point collides with a given shape, for various types of shape. (quark’s example has already shown how do do that for circles and rectangles). In openprocessing, for example, this library is easily enabled from the “libraries” section of the settings sidebar.

  4. If all the shapes you want to interact with are actually intended to be UI elements (buttons, etc), you can use p5’s createButton and assign a function that should be called when that button is clicked.

(I would start with quark’s solution).

You can indeed detect a cursor position over irregular shapes. Most Immediate Mode UI libraries do this. As quark says above it is very easy over rectangles and ellipses but polygons are harder as you have to store each points position as a vector. The following is a port of my C++ Immediate mode UI manager for openFrameworks to P5 as a class:

// UIManager + demo ported from the C++ ImmediateModeUI example

class Button {
  constructor() {
    this.position = createVector(0,0);
    this.size = createVector(0,0);
    this.rotation = 0.0; // degrees
    this.type = 'push';
    this.shape = 'rectangle';
    this.points = [];
    this.state = 'off';
    this.hovered = false;
    this.label = '';
    this.action = null;
    this.name = '';
  }
}

class UIManager {
  constructor() {
    this.buttons = [];
    this.registry = {};
  }

  addButton(bOrX, y, w, h, label = '', shape = 'rectangle', rotation = 0.0, action = null, type = 'push', name = '') {
    if (bOrX instanceof Button) {
      let b = bOrX;
      if (b.name && this.registry[b.name]) b.action = this.registry[b.name];
      this.buttons.push(b);
      return this.buttons[this.buttons.length-1];
    }
    // convenience overload
    let b = new Button();
    b.position = createVector(bOrX, y);
    b.size = createVector(w, h);
    b.rotation = rotation;
    b.label = label;
    b.shape = shape;
    b.type = type;
    b.name = name;
    if (name && this.registry[name]) b.action = this.registry[name]; else b.action = action;
    this.buttons.push(b);
    return this.buttons[this.buttons.length-1];
  }

  addPolygon(x, y, points, label = '', rotation = 0.0, action = null, type = 'push', name = '') {
    let b = new Button();
    b.position = createVector(x, y);
    if (points && points.length>0) {
      let minX = points[0].x, minY = points[0].y, maxX = points[0].x, maxY = points[0].y;
      for (let p of points) {
        minX = Math.min(minX, p.x); minY = Math.min(minY, p.y);
        maxX = Math.max(maxX, p.x); maxY = Math.max(maxY, p.y);
      }
      b.size = createVector(maxX - minX, maxY - minY);
    } else {
      b.size = createVector(0,0);
    }
    b.rotation = rotation;
    b.label = label;
    b.shape = 'polygon';
    b.type = type;
    b.points = points.map(p => createVector(p.x, p.y));
    b.name = name;
    if (name && this.registry[name]) b.action = this.registry[name]; else b.action = action;
    this.buttons.push(b);
    return this.buttons[this.buttons.length-1];
  }

  draw() {
    for (let button of this.buttons) {
      push();
      let center = p5.Vector.add(button.position, p5.Vector.mult(button.size, 0.5));
      translate(center.x, center.y);
      rotate(radians(button.rotation));

      if (button.hovered) fill(255, 220, 50);
      else if (button.shape === 'rectangle') fill(220,80,80);
      else if (button.shape === 'ellipse') fill(100,200,120);
      else fill(80,120,220);

      noStroke();
      if (button.shape === 'rectangle') {
        rectMode(CENTER);
        rect(0,0, button.size.x, button.size.y);
      } else if (button.shape === 'ellipse') {
        ellipse(0,0, button.size.x, button.size.y);
      } else if (button.shape === 'polygon') {
        beginShape();
        for (let p of button.points) {
          let v = p5.Vector.sub(p, p5.Vector.mult(button.size, 0.5));
          vertex(v.x, v.y);
        }
        endShape(CLOSE);
      }

      // label
      fill(0);
      textSize(12);
      textAlign(LEFT, TOP);
      text(button.label, -button.size.x/2 + 4, -button.size.y/2 + 4);

      pop();
    }
  }

  onMouseMoved(x, y) {
    for (let button of this.buttons) {
      let local = UIManager.worldToLocalCentered(x, y, button);
      let isHover = false;
      if (button.shape === 'rectangle') {
        let hw = button.size.x * 0.5;
        let hh = button.size.y * 0.5;
        isHover = (local.x >= -hw && local.x <= hw && local.y >= -hh && local.y <= hh);
      } else if (button.shape === 'ellipse') {
        let rx = button.size.x * 0.5;
        let ry = button.size.y * 0.5;
        if (rx > 0 && ry > 0) isHover = ((local.x*local.x)/(rx*rx) + (local.y*local.y)/(ry*ry)) <= 1.0;
      } else if (button.shape === 'polygon') {
        let poly = button.points.map(p => p5.Vector.sub(p, p5.Vector.mult(button.size,0.5)));
        isHover = UIManager.pointInPolygon(local.x, local.y, poly);
      }
      button.hovered = isHover;
    }
  }

  onMousePressed(x, y) {
    for (let button of this.buttons) {
      let local = UIManager.worldToLocalCentered(x, y, button);
      let isHit = false;
      if (button.shape === 'rectangle') {
        let hw = button.size.x * 0.5;
        let hh = button.size.y * 0.5;
        isHit = (local.x >= -hw && local.x <= hw && local.y >= -hh && local.y <= hh);
      } else if (button.shape === 'ellipse') {
        let rx = button.size.x * 0.5;
        let ry = button.size.y * 0.5;
        if (rx > 0 && ry > 0) isHit = ((local.x*local.x)/(rx*rx) + (local.y*local.y)/(ry*ry)) <= 1.0;
      } else if (button.shape === 'polygon') {
        let poly = button.points.map(p => p5.Vector.sub(p, p5.Vector.mult(button.size,0.5)));
        isHit = UIManager.pointInPolygon(local.x, local.y, poly);
      }

      if (isHit) {
        if (button.type === 'push') {
          if (button.action) button.action();
        } else if (button.type === 'toggle') {
          button.state = (button.state === 'on') ? 'off' : 'on';
          if (button.action) button.action();
        }
      }
    }
  }

  registerAction(name, fn) { this.registry[name] = fn; }
  hasAction(name) { return this.registry[name] !== undefined; }
  clear() { this.buttons = []; this.registry = {}; }
  getHoveredState(x,y) { return this.buttons.some(b => b.hovered); }

  static pointInPolygon(x, y, poly) {
    let inside = false;
    let n = poly.length;
    if (n < 3) return false;
    for (let i=0, j=n-1; i<n; j=i++) {
      let xi = poly[i].x, yi = poly[i].y;
      let xj = poly[j].x, yj = poly[j].y;
      let intersect = ((yi>y) !== (yj>y)) && (x < (xj-xi)*(y-yi)/(yj-yi) + xi);
      if (intersect) inside = !inside;
    }
    return inside;
  }

  static worldToLocalCentered(x, y, button) {
    let center = p5.Vector.add(button.position, p5.Vector.mult(button.size,0.5));
    let p = createVector(x - center.x, y - center.y);
    let angle = radians(-button.rotation);
    let c = Math.cos(angle), s = Math.sin(angle);
    return createVector(p.x * c - p.y * s, p.x * s + p.y * c);
  }
}

// ------ Demo sketch
let uiManager;

function setup() {
  createCanvas(1024, 768);
  uiManager = new UIManager();

  uiManager.addButton(100, 100, 50, 50, 'Button 1', 'rectangle', 0.0, () => action1(), 'push');
  uiManager.addButton(100, 200, 50, 50, 'Button 2', 'ellipse', 0.0, () => action2(), 'toggle');
  uiManager.addButton(200, 200, 100, 50, 'Button 3', 'ellipse', 0.0, () => action3(), 'toggle');
  uiManager.addPolygon(100, 300, [ {x:0,y:0}, {x:50,y:50}, {x:50,y:0} ], 'Button 4', 0.0, () => action4(), 'push');
  uiManager.addButton(250, 100, 80, 40, 'Button 5', 'rectangle', 45.0, () => action5(), 'push');

  noSmooth();
}

function draw() {
  background(240);
  uiManager.draw();
}

function mouseMoved() {
  uiManager.onMouseMoved(mouseX, mouseY);
}

function mousePressed() {
  uiManager.onMousePressed(mouseX, mouseY);
}

function action1(){ console.log('Action 1 executed!'); }
function action2(){ console.log('Action 2 executed!'); }
function action3(){ console.log('Action 3 executed!'); }
function action4(){ console.log('Action 4 executed!'); }
function action5(){ console.log('Action 5 executed!'); }

Obviously this is overkill but you can cut and paste and otherwise chop it about as you need.