Clickable Shapes

Exploring mouse and object interactions: Stage

Mouse and Object Manuevers!
F4
Power(JS), Style(CSS), Structure(HTML), Flexibility(BS5). Bootstrap5 too!

The 'Fantastic Four' (HTML, CSS, JavaScript and Bootstrap5) along with P5JS helped us explore the idea of 'clickable' objects!

This app was developed as a 'mini-lesson' in P5JS for our 2024-25 Game Development class. We started with a Simple TNT P5JS Template and learned a lot in the process!

The big ideas?
isMouseInCanvas()

Actions associated with the mouse (location, clicking) should be limited to times when the mouse is actually in the canvas. Otherwise clicking anywhere on the page might trigger actions

Here's the function source code:

function isMouseInCanvas(){
    return mouseX > 0 && mouseX < myWidth && mouseY > 0 && mouseY < myHeight;
}//end isMouseInCanvas
                                

Notice how we concatenated multiple conditions using 'logical and' (&&) the just returned the over-arching boolean condition

Updating Mouse Location with a DOM object and innerHTML

If the mouse isn't in the canvas, our mouseInfoSpan reports: 'TBD' but if it is in the canvas, we'd like to know where it is.

Previously, in the HTML file we created an object:

var mouseInfoSpan = document.getElementById("mouseInfoSpan");
                                

Now, within the draw function we can show where the mouse is, limiting the number of decimal places to only 3:

mouseInfoSpan.innerHTML = mouseX.toFixed(3) + ", " + mouseY.toFixed(3);
                                

This is the DOM at work! Reference an object, then target its content with innerHTML!

mousePressed()

The inclusion of this p5JS's function will trigger an event. Here, we register our 3 objects and request them to be 'click'ed. If they are clickable, they will do their jobs.

Here's the complete method:

function mousePressed(){
    //dwr! if you add extra elements, they must be put here!
    myCircle.click();
    myRectangle.click();
    myRoundedRect.click();
}//end mousePressed
                                

Read about each object's 'click' method (see below)

This work was made easier by having class code to make each object. See details below.

class TNTCircle

No surprise: we use classes to make objects for manipulation.

Here's the TNTCircle Class:

class TNTCircle{
    //properties, attributes, field variables
    constructor(center, radius, fcolr){
        this.center = center;
        this.radius = radius;
        this.fcolr = fcolr;
        this.clickColr = this.createClickColor();
        this.currentColr = this.fcolr;
        this.isClicked = false;
        this.clickCount = 0;
    }//end constructor

    render(){
        push();
        translate(this.center.x, this.center.y);
        noStroke();
        fill(this.currentColr);
        circle(0, 0, 2*this.radius); //3rd parameter is diameter!
        pop();
    }//end render

    isClickable(){
        var cStatus = false;
        if(dist(this.center.x, this.center.y, mouseX, mouseY) < this.radius){
            cStatus = true; 
        }

        return cStatus;
    }//end isClickable

    click(){
        if(this.isClickable()){
            console.log("...circle clicked!...");
            this.clickCount++;
            //using the modulus operator (%) to determine
            //if the clickCount is even or odd
            if(this.clickCount % 2 == 0){
                this.currentColr = this.fcolr;
                this.isClicked = true;
            }else{
                this.currentColr = this.clickColr;
                this.isClicked = false;
            }
        }else{
            return;
        }
    }//end click

    createClickColor(){
        var aHue = hue(this.fcolr);
        var aSat = .5*saturation(this.fcolr);
        var aBright = brightness(this.fcolr);
        var cc = color(aHue, aSat, aBright, 100);
        return cc;
    }//end createClickColor

}//end TNTCircle
                                
class TNTRectangle

The TNTRectangle has 2 'parameters' that determine it's shape and from a mouse-hovering perspective, we can get erroneous results if/when the shape is rotating. Study its isClickable() to get an idea of why!

Objects created with this class have the ability, potentially, to 'spin' and the render code shows how.

Here's the TNTRectangle Class:

class TNTRectangle{
    //properties, attributes, field variables
    constructor(center, width, height, fcolr, shouldSpin){
        this.center = center;
        this.width = width;
        this.height = height;
        this.fcolr = fcolr;
        this.clickColr = this.createClickColor();
        this.currentColr = this.fcolr;
        this.isClicked = false;
        this.clickCount = 0;
        this.shouldSpin = shouldSpin;
    }//end constructor

    render(){
        rectMode(CENTER);
        push();
        //dwr: translate, then rotate!
        translate(this.center.x, this.center.y);
        noStroke();
        fill(this.currentColr);
        if(this.shouldSpin){
            rotate(.1*frameCount);
        }
        rect(0, 0, this.width, this.height);
        pop();
    }//end render

    isClickable(){
        //checks by mouse location
        //problem: doesn't allow for rounded edges or rotations
        var cStatus = false;
        var horizCheck = (mouseX > this.center.x - .5*this.width) && (mouseX < this.center.x + .5*this.width);
        var vertCheck = (mouseY > this.center.y - .5*this.height) && (mouseY < this.center.y + .5*this.height);
        if(horizCheck && vertCheck){
            cStatus = true;   
        }

        return cStatus;
    }//end isClickable

    click(){
        if(this.isClickable()){
            console.log("...rectangle clicked!...");
            this.clickCount++;
            if(this.clickCount % 2 == 1){
                this.currentColr = this.clickColr;
                this.shouldSpin = true;
                this.isClicked = true;
            }else{
                this.currentColr = this.fcolr;
                this.shouldSpin = false;
                this.isClicked = false;
            }
        }else{
            return;
        }
    }//end click

    createClickColor(){
        var aHue = hue(this.fcolr);
        var aSat = .5*saturation(this.fcolr);
        var aBright = brightness(this.fcolr);
        var cc = color(aHue, aSat, aBright, 100);
        return cc;
    }//end createClickColor

}//end TNTRectangle
                                
class TNTRoundedRectangle

This class allows a rectangle to have rounded edges and in addition to spinning, its with can 'oscillate,' upon a mouse click.

Here's the TNTRoundedRectangle Class:

class TNTRoundedRectangle{
    constructor(center, width, height, bRadius, fcolr, shouldSpin){
        this.center = center;
        this.width0 = width; //original width: no oscillation
        this.width = width;  //functional width for rendering
        this.height = height;
        this.bRadius = bRadius; //border radius
        this.fcolr = fcolr;
        this.clickColr = this.createClickColor();
        this.currentColr = this.fcolr;
        this.isClicked = false;
        this.clickCount = 0;
        this.shouldSpin = shouldSpin;
        this.shouldOscillate = false;
    }//end constructor

    render(){
        rectMode(CENTER); //even works with CORNER mode (try it!)
        push();
        translate(this.center.x, this.center.y);
        noStroke();
        fill(this.currentColr);
        if(this.shouldSpin){
            rotate(-.2*frameCount);
        }
        if(this.shouldOscillate){
            this.width = this.width0 + .5*this.width0*Math.sin(.1*frameCount);
        }else{
            this.width = this.width0;
        }
        rect(0, 0, this.width, this.height, this.bRadius);
        pop();
    }//end render

    isClickable(){
        //using color check: helps when rotating or smoothed edges!
        //get rgb pixel info at a mouse location
        var aColor = get(mouseX, mouseY);
        //console.log("aColor = " + aColor);
        //get rgb of currentColr
        var theRed = Math.round(red(this.currentColr));
        var theGreen = Math.round(green(this.currentColr));
        var theBlue = Math.round(blue(this.currentColr));
        //confirm no weird rounding issues
        //console.log("theR,G, B = " + theRed + ", " + theGreen + ", " + theBlue);
        var status = ((parseInt(aColor[0]) == theRed) && (parseInt(aColor[1]) == theGreen) && (parseInt(aColor[2]) == theBlue));
        return status;  
    }//end isClickable

    click(){
        if(this.isClickable()){
            console.log("...rounded rectangle clicked!...");
            this.clickCount++;
            if(this.clickCount % 2 == 1){
                this.currentColr = this.clickColr;
                this.shouldSpin = true;
                this.shouldOscillate = true;
                this.isClicked = true;
            }else{
                this.currentColr = this.fcolr;
                this.shouldSpin = false;
                this.shouldOscillate = false;
                this.isClicked = false;
            }
        }else{
            return;
        }
    }//end click

    createClickColor(){
        var aHue = hue(this.fcolr);
        var aSat = saturation(this.fcolr);
        var aBright = 1.3*brightness(this.fcolr);
        if(aBright > 100) aBright = 100;
        var cc = color(aHue, aSat, aBright, 100);
        return cc;
    }//end createClickColor

}//end TNTRoundedRectangle
                                
Commonalities of the classes!!

If you examine each class, you notice a trend: they all have properties with a this prefix. That prefix is a reminder that each and every object make with a command like:

var rC = createVector(.8*myWidth, .2*myHeight);
myRoundedRect = new TNTRoundedRectangle(rC, .25*myWidth, .1*myHeight, 
                                        .05*myHeight, myGreen, true);
                                

has it's own set of properties: this object has these properties, this other object has those properties. The constructor assembles the ingredients used in the new line of code and grants them to each object that was created.


Each object has a render command that renders it on the screen. (We use 'render' rather than 'draw' since 'draw' is a p5js keyword. (Just being careful, you know!). Each 'render' call incorporates a fundamental p5js graphics command that makes a basic shape: circle or rect. We're just exploiting the ideas within a class format.

'render' uses the idea of push and pop so each object has its own coordinate system!


Regarding isClickable(): TNTRoundRectangle is unique in that it does not use geometry to establish 'proximity' to the object: It essentially uses color matching, as shown:

isClickable(){
    //using color check: helps when rotating or smoothed edges!
    //get rgb pixel info at a mouse location
    var aColor = get(mouseX, mouseY);
    //console.log("aColor = " + aColor);
    //get rgb of currentColr
    var theRed = Math.round(red(this.currentColr));
    var theGreen = Math.round(green(this.currentColr));
    var theBlue = Math.round(blue(this.currentColr));
    //confirm no weird rounding issues
    //console.log("theR,G, B = " + theRed + ", " + theGreen + ", " + theBlue);
    var status = ((parseInt(aColor[0]) == theRed) && (parseInt(aColor[1]) == theGreen) && (parseInt(aColor[2]) == theBlue));
    return status;  
}//end isClickable
                                

Here's how isClickable for TNTRoundedRectangles works:

  1. get secures an rgba array of the pixel that's clicked on
  2. We can use the red, green, and blue functions to get the rgbs for our this.currentColr
  3. If aColor rgbs all match this.currentColr rgbs, it means the mouse was in contact with the shape and voila! it's clickable!
  4. Notice that aColor is zero-based indexed array, as we'd expect

click code tells how each object should behave when clicked (if its clickable!). In all cases, the color changes (see below). In other cases, shouldSpin or shouldOscillate properties are toggled on and off for your enjoyment! :)

Each object has a clickCount property that maintains how many times an object is clicked. Based on whether it's even or odd (thanks, % operator (aka modulus operator)) we turn properties on or off or change colors.


createClickColor() finds a variation of the fcolr property (lighter, muted, etc) and uses it as elements are 'engaged.'

Each use the hue, saturation and brightness functions to grab the original HSB values from fcolr and then tweak them to create a clickColr property. Each are assigned to currentColr as the objects are clicked.

cursor Settings

If you examine the draw function, you'll note that it has code to 'sense' whether an object is 'clickable' (mouse is in proximity of the object) and if ANY object is indeed clickable, the cursor property is changed to a HAND setting. Otherwise, it's just the normal ARROW setting.

Here's the P5JS draw code:

function draw() {
    background(bgColor);
    myCircle.render();
    myRectangle.render();
    myRoundedRect.render();
    if(isMouseInCanvas()){
        mouseInfoSpan.innerHTML = mouseX.toFixed(3) + ", " + mouseY.toFixed(3);
        var circleClickable = myCircle.isClickable();
        var rectClickable = myRectangle.isClickable();
        var roundRectClickable = myRoundedRect.isClickable();
        if(circleClickable || rectClickable || roundRectClickable) cursor(HAND);
        else cursor(ARROW);
    }else{
        mouseInfoSpan.innerHTML = "TBD";
    }
    
}//end function draw
                                

It a nutshell that's what happens with each 'frame' of the app. Because we are using classes in the operation, the draw code is relatively streamlined and self-documenting

The moral? we love classes!


...loading...

Mouse Location: