Mouse and Object Manuevers!
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:
getsecures an rgba array of the pixel that's clicked on- We can use the
red,green, andbluefunctions to get the rgbs for ourthis.currentColr - If
aColorrgbs all matchthis.currentColrrgbs, it means the mouse was in contact with the shape and voila! it's clickable! - Notice that
aColoris 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: