How to group DOM elements and their methods in p5.js? - javascript

I had made a menu for a game in p5.js and I wanted the menu to be simple but well presented and very interactive. Also, I wanted to have a small piece of code. I have achieved the first conditions but I still think my code is very big.
I encourage you to please change/edit/delete my code and write the same idea in a better way
let modes = [];
var mode1, mode2, mode3, mode4;
function setup() {
createCanvas(400, 500);
mode1 = createP("Mode 1");
mode2 = createP("Mode 2");
mode3 = createP("Mode 3");
mode4 = createP("Mode 4");
mode1.class("mode");
mode2.class("mode");
mode3.class("mode");
mode4.class("mode");
modes = selectAll(".mode");
for (var i = 0; i < modes.length; i++) {
modes[i].style("font-size", "50px");
}
mode1.position(40, 115);
mode2.position(215, 115);
mode3.position(40, 300);
mode4.position(215, 300);
}
function draw() {
background("#befecd");
noFill();
strokeWeight(8);
rect(20, 100, 360, 360);
line(20, 280, 380, 280);
line(200, 100, 200, 460);
fill(0);
textSize(64);
text("MENU", 100, 70);
mode1.mouseOver(function () {
mode1.html("Mode 1<br>description");
mode1.style("font-size", "35px");
});
mode2.mouseOver(function () {
mode2.html("Mode 2<br>description");
mode2.style("font-size", "35px");
});
mode3.mouseOver(function () {
mode3.html("Mode 3<br>description");
mode3.style("font-size", "35px");
});
mode4.mouseOver(function () {
mode4.html("Mode 4<br>description");
mode4.style("font-size", "35px");
});
mode1.mouseOut(function () {
mode1.html("Mode 1");
mode1.style("font-size", "35px");
});
mode2.mouseOut(function () {
mode2.html("Mode 2");
mode2.style("font-size", "50px");
});
mode3.mouseOut(function () {
mode3.html("Mode 3");
mode3.style("font-size", "50px");
});
mode4.mouseOut(function () {
mode4.html("Mode 4");
mode4.style("font-size", "50px");
});
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.2/p5.min.js"></script>
This are some ideas I have thought about:
Making that elements on the HTML page
Keep the different properties on variable variables and have only one mouseOver() / mouseOut() function for all the DOM elements
As you can see, I have gotten all elements on another variable, but I haven’t used that. Shall I?
Change to another programming language. I don’t feel very confortable with p5.js sometimes when I do games or those types of things. I ask you for other programming languages (not the best or personal opinion, I only want to know other options)

The solution to the thing1, thing2, thing3... thingN in pretty much all languages is arrays and iteration (usually loops in imperative languages).
You can factor out the variable parts of each repeated chunk of logic and generalize to structure that represents the raw data, then loop over it and build your elements from those variables.
const modeData = [
{description: "foo foo foo", position: [40, 115]},
{description: "bar bar bar", position: [215, 115]},
{description: "baz baz baz", position: [40, 300]},
{description: "quux quux", position: [215, 300]},
];
const modes = [];
function setup() {
createCanvas(400, 500);
modeData.forEach(({description, position: [x, y]}, i) => {
const p = createP(`Mode ${i + 1}`);
modes.push(p);
p.class("mode");
p.style("font-size", "45px");
p.position(x, y);
p.mouseOver(() => {
p.html(`Mode ${i + 1}<br>${description}`);
p.style("font-size", "35px");
});
p.mouseOut(() => {
p.html(`Mode ${i + 1}`);
p.style("font-size", "50px");
});
});
}
function draw() {
background("#befecd");
noFill();
strokeWeight(8);
rect(20, 100, 360, 360);
line(20, 280, 380, 280);
line(200, 100, 200, 460);
fill(0);
textSize(64);
text("MENU", 100, 70);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.5.0/p5.min.js"></script>
As you can see, the mouse over/out listeners shouldn't be re-added on every frame in draw, only one time in setup.
As a UI/UX/app design aside, it's a bit odd to combine canvas drawing and DOM elements like this, especially when resizing is involved. You can see some odd flashing between mouseover/out at certain mouse positions when the description text is small. I'd likely use CSS-styled DOM <div> elements rather than canvas lines to create the boxes. If the text content is larger, the problem disappears, but the point stands since there are other layout issues that can occur. Your use case may be an exception, so this is just a rule of thumb.
Note also that you start at 45px, then return to 50px after mouseout. Maybe those two numbers should be the same.

Related

How can I make a collider for only the bottom of objects?

For a 2D top-down game made with Phaser, the player should be able to walk past the front and back of furniture, which I've achieved by changing the depth of the player according to the y value of their position. However, they should not be able to walk through furniture by moving down through it (or moving up through it). They also be blocked from walking through it by walking at the furniture from the sides when at the same y value (coming at the furniture from a higher/lower value is fine, they then just go behind/in front of the furniture).
I figure this could be made by blocking collisions between the bottom of the player sprite and the bottom x pixels of furniture. But I don't know how to do this and can't find anything in the documentation or online. I figure I can make small invisible objects that go in the bottom of the player and furniture, but is there a simpler way to achieve this effect?
I have the furniture grouped by depth, and for each depth have code like this:
create {
furnitureBack = this.physics.add.staticGroup();
furnitureBack.create(300, 80, 'furniture').setFrame(60).refreshBody();
furnitureBack.setDepth(2);
colliderBack = this.physics.add.collider(player, furnitureBack);
}
update {
colliderBack.active = false;
if (player.y > 75 && player.y < 85) {
colliderBack.active = true;
}
}
The problem with this is that it only works when the player enters the 75-85 y range without already overlapping with the furniture. If they enter that range by walking towards the furniture from the bottom, then the collider becomes active but the player is still able to walk through the item.
An easy and fast solution would be to use phaser physics, and just alter the size of the body.
Here the link to the body Documentation (you could even make the collision box small and round with setCircle, here is the link Documentation)
Here a working demo:
(the white box is the player the brown box, is a box)
document.body.style = 'margin:0;';
var config = {
type: Phaser.AUTO,
width: 536/2,
height: 183/2,
zoom: 2,
physics: {
default: 'arcade',
arcade: {
debug: true
}
},
scene: {
create,
update
},
banner: false
};
function create () {
this.add.text(10, 10, 'Use cursor-keys to move')
this.player = this.add.rectangle(50, 80, 20, 40, 0xffffff)
.setOrigin(.5, 1)
.setDepth(1);
this.box = this.add.rectangle(100, 80, 20, 20, 0x933B26)
.setOrigin(.5, 1)
.setDepth(2);
this.physics.add.existing(this.box, true)
.body
//Setup physics-body size and position
.setSize(20, 10, false)
.setOffset(0, 10);
this.physics.add.existing(this.player)
.body
//Setup physics-body size and position
.setSize(20, 20, false)
.setOffset(0, 20);
this.keys = this.input.keyboard.createCursorKeys();
this.physics.add.collider(this.player, this.box)
}
function update(){
if(!this.player || !this.player.body){
return;
}
let body = this.player.body;
let speed = 50;
body.setVelocity(0, 0);
// Since the box depth is set to "2"
// you just have to alternate the depth of the player
let newDepth = (this.player.y > this.box.y) ? 3 : 1;
this.player.setDepth(newDepth);
if(this.keys.right.isDown){
body.setVelocity(speed, 0);
}
if(this.keys.left.isDown){
body.setVelocity(-speed, 0);
}
if(this.keys.up.isDown){
body.setVelocity(0, -speed);
}
if(this.keys.down.isDown){
body.setVelocity(0, speed);
}
}
new Phaser.Game(config);
<script src="https://cdn.jsdelivr.net/npm/phaser#3.55.2/dist/phaser.js"></script>

How to modify TimelineLite timing?

I’m quite new with the GreenSock and I got myself in trouble...
I would like to modify GreenSock TimelineLite timing offset for reverse so that some delays get deleted (I think that they are called staggers).
Here is an example: http://jsfiddle.net/4bvnv1d5/
var red = $('.red');
var green = $('.green');
var blue = $('.blue');
var black = $('.black');
var tl = new TimelineLite({onReverseComplete:reverseCompleted});
$('#start').click(function(){
tl.to(red, 0.3, {ease: Power1.easeInOut, 'margin-left':'100px'});
tl.to(green, 0.3, {ease: Power1.easeInOut, 'margin-left':'100px'});
tl.to(blue, 0.3, {ease: Power1.easeInOut, 'margin-left':'100px'});
tl.to(black, 0.3, {ease: Power1.easeInOut, 'margin-left':'100px', onComplete:lastCompleted, onCompleteParams:[black]}, "+=4");
});
$('#reverse').click(function(){
tl.reverse();
});
function lastCompleted(target) {
console.log('lastCompleted');
}
function reverseCompleted(){
console.log('reverseCompleted');
tl.clear();
tl.restart();
}
On play there is a four second delay with the last box, but on the reverse I’d like to animations to play right after each other with no delays. There is function lastCompleted() which is triggered after the last tween gets run. How can I use that function to remove the delay between the black and blue box animations?
Thanks!
Take a look at this fiddle.
JavaScript:
var red = $('.red');
var green = $('.green');
var blue = $('.blue');
var black = $('.black');
var tl = new TimelineLite({
paused: true,
callbackScope: this,
onReverseComplete: onTlReverseComplete
});
tl.staggerTo([red, green, blue], 0.3, { marginLeft: 100, ease: Power1.easeInOut }, 0.3);
tl.addLabel('MyLabel');
tl.to(black, 0.3, { marginLeft: 100, ease: Power1.easeInOut, onReverseComplete: onBlackBoxReverseComplete, callbackScope: this }, '+=4');
$('#start').click(function () {
tl.play();
});
$('#reverse').click(function () {
tl.reverse();
});
function onBlackBoxReverseComplete() {
tl.reverse('MyLabel');
}
function onTlReverseComplete() {
tl.stop();
}
Quite a few things have changed from your code. Here is the list:
The tweens are added into the tl instance outside the scope of click handler of #start button. The click handler only .play()s the timeline forward.
.staggerTo() method is used instead of adding adding the 3 tweens one by one before the one for .black element.
Also, margin-left has been replaced by its JS equivalent marginLeft and since it accepts numbers and defaults to pixels, no need to pass the values as strings.
The tween for .black element has now a onReverseComplete callback.
Hope this helps. Let me know if you have any questions.

How to prevent loss hover in Raphael?

I'm developing some page when I use Raphael liblary to draw some items.
my App
So my problem is in that when I'm moving to some rect it growing up but when my mouse is on text which is positioning on my rect, it loss his hover. You can see it on my app example.
var paper = new Raphael(document.getElementById('holder'), 500, object.length * 100);
drawLine(paper, aType.length, bType.length, cType.length, cellSize, padding);
process = function(i,label)
{
txt = paper.text(390,((i+1)* cellSize) - 10,label.devRepo)
.attr({ stroke: "none", opacity: 0, "font-size": 20});
var a = paper.rect(200, ((i+1)* cellSize) - 25, rectWidth, rectHeight)
.hover(function()
{
this.animate({ transform : "s2"}, 1000, "elastic");
this.prev.animate({opacity: 1}, 500, "elastic");
this.next.attr({"font-size" : 30});
},
function()
{
this.animate({ transform : "s1" }, 1000, "elastic");
this.prev.animate({opacity: 0}, 500);
this.next.attr({"font-size" : 15});
});
}
I have tried e.preventDefault(); on hover of this.next and some other solutions but it's doesn't work.
Any help would be appreciated.
Most people will suggest you place a transparent rectangle over the box and the labels and attach the hover functions to that instead. (If memory serves, you have to make the opacity 0.01 instead of 0 to prevent the object from losing its attached events.) This works fine, but I don't love this solution; it feels hacky and clutters the page with unnecessary objects.
Instead, I recommend this: Remove the second function from the hover, making it functionally a mouseover function only. Before you draw any of the rectangles and labels, make a rectangular "mat" the size of the paper. Then, attach the function that minimizes the label as a mouseover on the mat. In other words, you're changing the trigger from mousing out of the box to mousing over the area outside of it.
I left a tiny bit of opacity and color on the mat to be sure it's working. You can just change the color to your background color.
var mat = paper.rect(0, 0, paper.width, paper.height).attr({fill: "#F00", opacity: 0.1});
Now, you want to make a container for all the rectangles so you can loop through them to see which need to be minimized. I made an object called "rectangles" that contains the objects we're concerned with. Then:
mat.mouseover(function () {
for (var c = 0; c < rectangles.length; c += 1) {
//some measure to tell if rectangle is presently expanded
if (rectangles[c].next.attr("font-size")) {
rectangles[c].animate({
transform : "s1"
}, 1000, "elastic");
rectangles[c].prev.animate({opacity: 0}, 500);
rectangles[c].next.attr({"font-size" : 15});
}
}
});
Then I just removed the mouseout function from the individual rectangles.
jsBin
To be clear, this will have some downsides: If people run the mouse around really fast, they can expand several rectangles at the same time. This is remedied as soon as the mouse touches the mat. I think the functionality looks pretty nice. But the invisible mats is always an option.
I wrote a small extension to Raphael - called hoverInBounds - that resolves this limitation.
Demo: http://jsfiddle.net/amustill/Bh276/1
Raphael.el.hoverInBounds = function(inFunc, outFunc) {
var inBounds = false;
// Mouseover function. Only execute if `inBounds` is false.
this.mouseover(function() {
if (!inBounds) {
inBounds = true;
inFunc.call(this);
}
});
// Mouseout function
this.mouseout(function(e) {
var x = e.offsetX || e.clientX,
y = e.offsetY || e.clientY;
// Return `false` if we're still inside the element's bounds
if (this.isPointInside(x, y)) return false;
inBounds = false;
outFunc.call(this);
});
return this;
}

javascript raphael object function passing

I apologize for asking this question but I am just looking for a little guidance on this morning. I simply want to create a function so that way I can make a Raphael element glow by just passing in that element. Below is the code I have. Why does this not work?
var paper = Raphael("playarea", 500, 500);
var rectangle = paper.rect(100, 100, 200, 200, 4);
function elemHover(var el)
{
el.hover(
// When the mouse comes over the object //
// Stock the created "glow" object in myCircle.g
function() {
this.g = this.glow({
color: "#0000EE",
width: 10,
opacity: 0.8
});
},
// When the mouse goes away //
// this.g was already created. Destroy it!
function() {
this.g.remove();
});
}
elemHover(rectangle);
here is the fiddle http://jsfiddle.net/aZG6C/15/
You should fill the element( rectangle in our case) to trigger the hover.
rectangle.attr("fill", "red");
Try this fiddle http://jsfiddle.net/aZG6C/17/
The full code will look like
<div id="playarea"></div>
​<script type="text/javascript">
var paper = Raphael("playarea", 500, 500);
var rectangle = paper.rect(100, 100, 200, 200, 4);
function elemHover(el)
{
el.hover(
// When the mouse comes over the object //
// Stock the created "glow" object in myCircle.g
function() {
this.g = this.glow({
color: "#0000EE",
width: 10,
opacity: 0.8
});
},
// When the mouse goes away //
// this.g was already created. Destroy it!
function() {
this.g.remove();
});
}
rectangle.attr("fill", "red");
elemHover(rectangle);
</script>​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​
Update
Hover event is triggered only if the element is filled with something. If you want to have a transparent element you can try
rectangle.attr("fill", "transparent");
Check the fiddle here http://jsfiddle.net/aZG6C/20/

How to update the source of an Image

I'm using the Raphaël Javascript lib (awesome stuff for SVG rendering, by the way) and am currently trying to update the source of an image as the mouse goes over it.
The thing is I can't find anything about it (it's probably not even possible, considering I've read a huge part of the Raphaël's source without finding anything related to that).
Does someone knows a way to do this ?
Maybe it can be done without directly using the Raphaël's API, but as the generated DOM elements doesn't have IDs I don't know how to manually change their properties.
I'm actually doing CoffeeScript, but it's really easy to understand. CoffeeScript is Javascript after all.
This is what I'm doing right know, and I would like the MouseOver and MouseOut methods to change the source of the "bg" attribute.
class Avatar
constructor: (father, pic, posx, posy) ->
#bg = father.container.image "pics/avatar-bg.png", posx, posy, 112, 112
#avatar = father.container.image pic, posx + 10, posy + 10, 92, 92
mouseOver = => #MouseOver()
mouseOut = => #MouseOut()
#bg.mouseover mouseOver
#bg.mouseout mouseOut
MouseOver: ->
#bg.src = "pics/avatar-bg-hovered.png"
alert "Hover"
MouseOut: ->
#bg.src = "pics/avatar-bg.png"
alert "Unhovered"
class Slider
constructor: ->
#container = Raphael "raphael", 320, 200
#sliderTab = new Array()
AddAvatar: (pic) ->
#sliderTab.push new Avatar this, pic, 10, 10
window.onload = ->
avatar = new Slider()
avatar.AddAvatar "pics/daAvatar.png"
This actually works, except for the "#bg.src" part : I wrote it knowing that it wouldn't work, but well...
var paper = Raphael("placeholder", 800, 600);
var c = paper.image("apple.png", 100, 100, 600, 400);
c.node.href.baseVal = "cherry.png"
I hope, you get the idea.
This works for me (and across all browsers):
targetImg.attr({src: "http://newlocation/image.png"})
I was using rmflow's answer until I started testing in IE8 and below which returned undefined for image.node.href.baseVal. IE8 and below did see image.node.src though so I wrote functions getImgSrc, setImgSrc so I can target all browsers.
function getImgSrc(targetImg) {
if (targetImg.node.src) {
return targetImg.node.src;
} else {
return targetImg.node.href.baseVal;
}
}
function setImgSrc(targetImg, newSrc) {
if (targetImg.node.src) {
targetImg.node.src = newSrc;
} else {
targetImg.node.href.baseVal = newSrc;
}
}

Categories