I wrote this simple carousel but without encapsulation. So previously I placed items from buttonControl() in global scope and added eventListeners on global scope, which are now emraced in prev() and next() functions. However, my encapsulation breaks the code. Because arguments from buttonControl() aren't global but prev() and next() needs them to work. I thought that maybe I can pass all arguments from buttonsControl() inside addEventListener('click', prev) but I cannot, because when I write thisaddEventListener('click', prev(slides,totalItems,allItems....)) it is launching this event without a click.And I even don't know if its correct way.
I thought of puting arguments from buttonsControl() inside prev() and next() but it won't work.
function buttonsControl(){
const slides = document.querySelector('.slides');
const totalItems = document.querySelectorAll('.slides>*').length - 1;
const allItems = document.querySelectorAll('.slides>*').length;
console.log(totalItems)
let activeItem = 0;
let controlCarouselFooter = document.querySelector('.carousel_footer');
controlCarouselFooter.innerHTML = `1 / ${allItems}`
console.log(controlCarouselFooter)
const prevButton = document.querySelector('.prev_button').addEventListener('click', prev)
const nextButton = document.querySelector('.next_button').addEventListener('click', next)
// no idea how to pass those arguments
}
// Buttons controls
function prev(){
// const prevButton = document.querySelector('.prev_button').addEventListener('click', () => {
if (activeItem === 0) {
activeItem = totalItems;
slides.style.transform = `translateX(-${totalItems * 100}%)`;
console.log(`if ${activeItem}`)
controlCarouselFooter.innerHTML = `${activeItem+1} / ${allItems}`
}else {
activeItem--;
slides.style.transform = `translateX(-${activeItem * 100}%)`;
console.log(`else ${activeItem}`)
controlCarouselFooter.innerHTML = `${activeItem+1} / ${allItems} `
}
}
// );
// }
function next(){
// const nextButton = document.querySelector('.next_button').addEventListener('click', () => {
if(activeItem < totalItems) {
activeItem++;
slides.style.transform = `translateX(-${activeItem * 100}%)`;
console.log(`if ${activeItem}`)
controlCarouselFooter.innerHTML = `${activeItem+1} / ${allItems}`
} else {
activeItem = 0;
slides.style.transform = 'none';
console.log(`else ${activeItem+1}`)
console.log(`totalItems ${totalItems}`)
controlCarouselFooter.innerHTML = `${activeItem+1} / ${allItems}`
}
}
// );
// };
// });
buttonsControl();
The easiest solution would be to define the functions prev and next inside the buttonsControl function, so that all its local variables are in scope through closure:
function buttonsControl() {
const slides = document.querySelector('.slides');
const totalItems = document.querySelectorAll('.slides>*').length - 1;
const allItems = document.querySelectorAll('.slides>*').length;
let activeItem = 0;
let controlCarouselFooter = document.querySelector('.carousel_footer');
controlCarouselFooter.innerHTML = `1 / ${allItems}`;
const prevButton = document.querySelector('.prev_button').addEventListener('click', prev);
const nextButton = document.querySelector('.next_button').addEventListener('click', next);
// Buttons controls
function prev() {
if (activeItem === 0) {
activeItem = totalItems;
} else {
activeItem--;
}
slides.style.transform = `translateX(-${totalItems * 100}%)`;
controlCarouselFooter.innerHTML = `${activeItem+1} / ${allItems}`
}
function next() {
if (activeItem < totalItems) {
activeItem++;
slides.style.transform = `translateX(-${activeItem * 100}%)`;
} else {
activeItem = 0;
slides.style.transform = 'none';
}
controlCarouselFooter.innerHTML = `${activeItem+1} / ${allItems}`
}
}
buttonsControl();
If I'm understanding your question correctly, You could bind the variables to the listeners.
EDIT: Someone pointed out that you're mutating activeItems. Which is true. So You will want to define all your variables on an object first so that mutation is persistent between function calls.
function buttonsControl(){
let obj = {};
obj.slides = document.querySelector('.slides');
obj.totalItems = document.querySelectorAll('.slides>*').length - 1;
obj.allItems = document.querySelectorAll('.slides>*').length;
console.log(obj.totalItems)
obj.activeItem = 0;
obj.controlCarouselFooter = document.querySelector('.carousel_footer');
controlCarouselFooter.innerHTML = `1 / ${allItems}`
console.log(controlCarouselFooter)
//bind the variables you want to use to your input
const prevButton = document.querySelector('.prev_button').addEventListener('click', prev.bind(null, obj))
const nextButton = document.querySelector('.next_button').addEventListener('click', next.bind(null, obj))
// no idea how to pass those arguments
}
// Buttons controls
function prev(obj){
//define the arguments in your fn params.
// const prevButton = document.querySelector('.prev_button').addEventListener('click', () => {
if (obj.activeItem === 0) {
obj.activeItem = totalItems;
obj.slides.style.transform = `translateX(-${totalItems * 100}%)`;
console.log(`if ${activeItem}`)
obj.controlCarouselFooter.innerHTML = `${obj.activeItem+1} / ${obj.allItems}`
}else {
obj.activeItem--;
obj.slides.style.transform = `translateX(-${activeItem * 100}%)`;
console.log(`else ${obj.activeItem}`)
obj.controlCarouselFooter.innerHTML = `${obj.activeItem+1} / ${obj.allItems} `
}
}
// );
// }
function next(obj){
...similar implimentation to prev()
}
// );
// };
// });
buttonsControl();
I have a function called debouncedGetScrolledDownPercentage that I want to return the scrollPercentage. As it is debounced, it incorrectly returns undefined, presumably because the debounce function does not return anything.
How can debouncedGetScrolledDownPercentage() return the scrollPercentage, so that scrolledDownPercentage: debouncedGetScrolledDownPercentage() is not undefined? I assume a global variable accessible to all scopes could solve it, but I would prefer a different solution.
Any help would be appreciated, thank you.
For context, here is all the code:
window.onbeforeunload = function() { return "Are you sure you want to leave?"; }; // for testing, so you can click close and the browser won't close and you can still see the logs
// initial data
let bounced = true;
const sessionStartTime = new Date();
let maxScrollPosition = 0;
let totalScrollDistance = 0;
// functions
const setBouncedToFalse = () => {
bounced = false;
document.removeEventListener("click", setBouncedToFalse);
};
const calculateSessionTime = () => {
const sessionEndTime = new Date();
return sessionEndTime - sessionStartTime; // sessionTimeMs
}
const debounce = (func, delay) => {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
};
const debouncedGetScrolledDownPercentage = debounce(() => {
// const debouncedGetScrolledDownPercentage = debounce((callback) => {
const scrollPosition = window.scrollY;
let scrollPercentage = 0;
if (scrollPosition > maxScrollPosition) {
const scrollDistance = scrollPosition - maxScrollPosition;
totalScrollDistance += scrollDistance;
maxScrollPosition = scrollPosition;
const maxPossibleScrollDistance = document.documentElement.scrollHeight - window.innerHeight;
scrollPercentage = Math.round(totalScrollDistance / maxPossibleScrollDistance * 100);
}
return scrollPercentage;
// callback(scrollPercentage);
}, 200);
const processData = () => {
const userData = {
sessionTimeMs: calculateSessionTime(),
bounced,
scrolledDownPercentage: debouncedGetScrolledDownPercentage() // incorrectly returns undefined
};
debouncedGetScrolledDownPercentage((scrollPercentage) => {
userData.scrolledDownPercentage = scrollPercentage;
console.log('userData: ', userData);
});
}
// events
document.addEventListener("click", setBouncedToFalse); // track clicks (if user bounces)
window.addEventListener('scroll', debouncedGetScrolledDownPercentage); // needed?
window.addEventListener("beforeunload", processData); // process data before user leaves
/Result got printed. But don't understand why its showing [[PromiseState]]: "rejected", it should be "fulfilled", what was the mistake ,i made in the code??/
let counterElement = document.querySelector(".counter");
const promiseCheck = new Promise((resolve, reject) => {
let Timer = setInterval(() => {
var runner = parseInt(document.querySelector(".counter").innerText);
counterElement.innerText = runner - 1;
if (runner === 0) {
clearInterval(Timer);
resolve((counterElement.innerText = "Happy Morning"));
}else{
reject("wht going wrong")
}
// console.log(runner)
}, 1000);
});
console.log(promiseCheck);
promiseCheck
.then((data) => console.log(data))
.catch((errData) => console.log(errData));
index.html
<h1 class="counter">10</h1>
You can use requestAnimationFrame to achieve this result cleanly. It works like setTimeout and setInterval (which could also be employed on those lines).
const timer = document.getElementById("time");
var startTime = null;
var endTime = null;
function changeTimer() {
if (startTime === null) {
startTime = Date.now();
/* Set up the start time to be 11 seconds (11,000 ms)
in the future so it displays 10 for a whole second */
endTime = startTime + 11000;
requestAnimationFrame(changeTimer);
return;
}
let nValue = Date.now();
let tValue = (nValue < endTime) ? endTime - nValue : 0;
let mtValue = tValue / 1000;
let mtiValue = parseInt(mtValue);
if (mtiValue < 1) {
timer.innerText = "Happy Morning";
return;
}
timer.innerText = mtiValue;
requestAnimationFrame(changeTimer);
return;
}
changeTimer();
<h1 class="counter" id="time">10</h1>
I'm building a video game where a spaceship moves with controllers and it must avoid the fireball in order to continue to play. If it collides into the fireball the game must display "Game Over" and restart.
At the beginning of the game, there is an input where the user puts his name. Then there is a countdown and then the game starts. I would like that the user gets back to the countdown instead of the point where he must input his name. Does someone knows how to do this?
Code for input:
<form id="askName" title="Write your name">
<label> Enter your username: </label>
<input id="input" type="text" maxlength="10" autofocus>
<button type="button" onclick="countDown(); return Username()" id="begin-timer">
Submit
</button>
let icon = document.getElementById("icon")
let fireballElement = document.querySelector("#fireball")
var input = document.getElementById("input")
function Username(field) {
field = input.value
if (field == "") { alert("Complete blanks"); return false }
document.getElementById("askName").style.display = "none"
setTimeout(function() {
document.getElementById("name").innerHTML = "Player: " + field
icon.style.display = 'block'
fireballElement.style.display = "block"
checkCollision()
}, 4000)
}
CountDown
var count = 3
function countDown() {
function preventCountFast() {
document.getElementById("count").innerHTML = count
if (count > 0) { count-- }
else {
clearInterval(ncount);
document.getElementById("count").style.display = "none"
}
}
var ncount = setInterval(preventCountFast, 1000)
}
Detect collision when fireball touches spaceship:
function checkCollision() {
var elem = document.getElementById("icon")
var elem2 = document.getElementById("fireball")
if (detectOverlap(elem, elem2) && elem2.getAttribute('hit') == 'false') {
hits++
elem2.setAttribute('hit', true)
//THIS IS WHERE YOU SHOULD LOOK AT
document.querySelector("#stopGame").style.display = "inline"
}
setTimeout(checkCollision, 20)
}
var detectOverlap = (function() {
function getPositions(elem) {
var pos = elem.getBoundingClientRect()
return [[pos.left, pos.right], [pos.top, pos.bottom]]
}
function comparePositions(p1, p2) {
var r1, r2
r1 = p1[0] < p2[0] ? p1 : p2
r2 = p1[0] < p2[0] ? p2 : p1
return r1[1] > r2[0] || r1[0] === r2[0]
}
return function(a, b) {
var pos1 = getPositions(a), pos2 = getPositions(b)
return comparePositions(pos1[0], pos2[0]) && comparePositions(pos1[1], pos2[1])
}
})()
<img src="Photo/fireball.png" id="fireball" style="display:none>
<img src="Photo/Spaceship1.png" id="icon" style="display:none">
<h2 id="stopGame"> Game Over! </h2>
Fireball movement:
function fFireball(offset) {
return Math.floor(Math.random() * (window.innerWidth - offset))
}
let fireball = { x: fFireball(fireballElement.offsetWidth), y: 0 }
const fireLoop = function() {
fireball.y += 1
fireballElement.style.top = fireball.y + 'px'
if (fireball.y > window.innerHeight) {
fireball.x = fFireball(fireballElement.offsetWidth)
fireballElement.style.left = fireball.x + 'px'
fireball.y = 0
fireballElement.setAttribute('hit', false )
}
}
fireballElement.style.left = fireball.x + 'px'
let fireInterval = setInterval(fireLoop, 1000 / 200)
Spaceship movement:
//Spaceship moves into space + prevent going out borders
let hits = 0
let display = document.getElementById("body")
let rect = icon
let pos = { top: 1000, left: 570 }
const keys = {}
window.addEventListener("keydown", function(e) { keys[e.keyCode] = true })
window.addEventListener("keyup" , function(e) { keys[e.keyCode] = false })
const loop = function() {
if (keys[37] || keys[81]) { pos.left -= 10 }
if (keys[39] || keys[68]) { pos.left += 10 }
if (keys[38] || keys[90]) { pos.top -= 10 }
if (keys[40] || keys[83]) { pos.top += 10 }
var owidth = display.offsetWidth
var oheight = display.offsetHeight
var iwidth = rect.offsetWidth
var iheight = rect.offsetHeight
if (pos.left < 0) pos.left = -10
if (pos.top < 0) pos.top = -10
if (pos.left + iwidth >= owidth ) pos.left = owidth - iwidth
if (pos.top + iheight >= oheight) pos.top = oheight- iheight
rect.setAttribute("data", owidth + ":" + oheight)
rect.style.left = pos.left + "px"; rect.style.top = pos.top + "px"
}
let sens = setInterval(loop, 1000 / 60)
Convert your countDown() to this:
function countDown(count) {
function preventCountFast() {
document.getElementById("count").innerHTML = count
document.getElementById("count").style.display = "block"
if (count > 0) { count-- }
else { clearInterval(ncount); document.getElementById("count").style.display = "none" }
}
var ncount = setInterval(preventCountFast, 1000)
}
So, while calling this, always call it as countDown(3) it will be easier. Next, modify your checkCollision() just a bit:
function checkCollision() {
var elem = document.getElementById("icon")
var elem2 = document.getElementById("fireball")
if (detectOverlap(elem, elem2) && elem2.getAttribute('hit') == 'false') {
hits++
elem2.setAttribute('hit', true)
document.querySelector("#stopGame").style.display = "block"
document.querySelector("#stopGame").style.animation = "seconds 5s forwards"
/* Added these three lines so that countDown() is called again if collision
occurs*/
setTimeout(function () {
countDown(5);
}, 2000);
/* End of edit */
}
setTimeout(checkCollision, 1)
}
Create a restart function and call it at the end of checkCollision like below.
function checkCollision() {
var elem = document.getElementById("icon")
var elem2 = document.getElementById("fireball")
if (detectOverlap(elem, elem2) && elem2.getAttribute('hit') == 'false') {
hits++
elem2.setAttribute('hit', true)
document.querySelector("#stopGame").style.display = "block"
document.querySelector("#stopGame").style.animation = "seconds 5s forwards"
// call restart at end of game
restart();
return;
}
setTimeout(checkCollision, 1)
}
function restart() {
// clear fireball animation
clearInterval(fireInterval);
count = 3;
let countElement = document.getElementById("count");
let stopGame = document.querySelector("#stopGame");
// show game over for 3 seconds to user
setTimeout(function () {
stopGame.style.animation = "";
stopGame.style.display = icon.style.display = fireballElement.style.display =
"none";
countElement.innerHTML = "";
countElement.style.display = "block";
countElement.style.transform = "scale(1)";
// allow count rerender
setTimeout(function () {
fireballElement.setAttribute("hit", false);
countDown();
// wait for count down to be over
setTimeout(function () {
// reset player position
pos.left = display.offsetWidth / 2;
pos.top = display.offsetHeight;
icon.style.display = "block";
// reset fireball position
fireball = { x: fFireball(display.offsetWidth / 2), y: 0 };
fireballElement.style.top = fireball.y + "px";
fireballElement.style.left = fireball.x + "px";
fireballElement.style.display = "block";
clearInterval(sens);
// restart loop list
sens = setInterval(loop, 1000 / 60);
fireInterval = setInterval(fireLoop, 1000 / 200);
checkCollision();
}, 4000);
}, 1000);
}, 3000);
}
Separate logic from presentation
The reason why you encountered this problem is that your code is tightly coupled to the presentation (DOM), and makes use of the global scope.
Your game logic should be separated from the presentation - DOM is basically an I/O device. Otherwise you tie yourself to a particular implementation (imagine refactoring this to a React application or using Material design, etc).
The principle is called "separation of concerns" (or SoC), and is a well-known principle of software design that will serve you well in the future.
Part 1. Counter
If you rely on a global variable like count to launch the timer, you will inevitably encounter issues with resetting it - make the state internal and only pass in configuration (start, end, and what to do on each step).
function getCounter({
init = 3,
onEnd = () => {},
onStep = () => {},
until = 0
} = {}) {
let curr = init;
return {
interval : null,
start() {
this.interval = setInterval(() => {
onStep(this, curr--);
const shouldStop = until === curr;
shouldStop && this.stop();
}, 1e3);
},
stop() {
const { interval } = this;
clearInterval(interval);
onEnd();
}
};
}
const counter = getCounter({
onStep : (counter, curr) => console.log(curr),
onEnd : () => console.log("ended")
});
counter.start();
Part 2. Game Over
Instead of controlling the UI upon collision, control what should happen, and defer coupling to a particular API:
const detectOverlap = () => !!Math.floor( Math.random() * 8 ); //mock for testing
const checkCollision = ({
obj1,
obj2,
interval = 20,
curHits = 0,
maxHits = 0,
onMiss,
onHit,
onGameOver
}) => {
const isHit = detectOverlap(obj1, obj2);
isHit && curHits++;
const hitOrMissConfig = {
curHits,
maxHits,
obj1,
obj2
};
isHit ? onHit(hitOrMissConfig) : onMiss(hitOrMissConfig);
const timeout = setTimeout(
() => checkCollision({
obj1, obj2, interval,
curHits, maxHits,
onHit, onMiss, onGameOver
}),
interval
);
if (curHits >= maxHits) {
clearTimeout(timeout);
return onGameOver();
}
};
const obj1 = { id : 1 };
const obj2 = { id : 2 };
const onHit = () => console.log("hit!");
const onMiss = () => console.log("miss!");
const onGameOver = () => console.log("game over!");
checkCollision({ obj1, obj2, onHit, onMiss, onGameOver, maxHits : 8 });
Part 3. Start logic
Instead of starting the game from the name form, you should encapsulate your logic and call it as a callback - this way you will have control over when to initiate the name form, countdown or anything else:
const loadForm = ({ parent = document.body, defaultUname, onSubmit } = {}) => {
const form = document.createElement("form");
const input = document.createElement("input");
input.name = "name";
input.type = "text";
input.value = defaultUname;
const start = document.createElement("button");
start.type = "button";
start.innerText = "Start";
form.append(input, start);
parent.append(form);
start.addEventListener("click", (event) => {
onSubmit({ name : input.value, firstTime : false });
form.remove();
});
};
//mocks for testing
const checkCollision = () => console.log("checking collision");
const countDown = (init) => {
if(init) {
console.log(init);
setTimeout(() => countDown(--init), 1e3);
}
};
const startGame = ({ name = "Player 1", firstTime = true } = {}) => {
let restarted = false;
if(!restarted && firstTime) {
return loadForm({
defaultUname : name,
onSubmit : startGame
});
}
countDown(3);
};
startGame();
All Steps combined
You will have to implement the UI handling, guards against no name, and connect collision detection, but this should take care of all the core logic. You might also want to make your fireball and spaceship proper JavaScript objects, and not impose logic on DOM elements for the reasons stated above.
function getCounter({
init = 3,
onEnd = () => {},
onStep = () => {},
until = 0
} = {}) {
let curr = init;
return {
interval : null,
start() {
this.interval = setInterval(() => {
onStep(this, curr--);
const shouldStop = until === curr;
shouldStop && this.stop();
}, 1e3);
},
stop() {
const { interval } = this;
clearInterval(interval);
onEnd();
}
};
}
const detectOverlap = () => !!Math.floor(Math.random() * 4); //mock for testing
const checkCollision = ({
obj1,
obj2,
interval = 20,
curHits = 0,
maxHits = 0,
onMiss,
onHit,
onGameOver
}) => {
const isHit = detectOverlap(obj1, obj2);
isHit && curHits++;
const hitOrMissConfig = {
curHits,
maxHits,
obj1,
obj2
};
isHit ? onHit(hitOrMissConfig) : onMiss(hitOrMissConfig);
const timeout = setTimeout(
() => checkCollision({
obj1, obj2, interval,
curHits, maxHits,
onHit, onMiss, onGameOver
}),
interval
);
if (curHits >= maxHits) {
clearTimeout(timeout);
return onGameOver();
}
};
const loadForm = ({
parent = document.body,
defaultUname,
onSubmit
} = {}) => {
const form = document.createElement("form");
const input = document.createElement("input");
input.name = "name";
input.type = "text";
input.value = defaultUname;
const start = document.createElement("button");
start.type = "button";
start.innerText = "Start";
form.append(input, start);
parent.append(form);
start.addEventListener("click", (event) => {
onSubmit({
name: input.value,
firstTime: false
});
form.remove();
});
};
const startGame = ({
name = "Player 1",
firstTime = true
} = {}) => {
if (firstTime) {
return loadForm({
defaultUname: name,
onSubmit: startGame
});
}
console.log(`Get ready, ${name}`);
const counter = getCounter({
onStep : (_,count) => console.log(count),
onEnd : () => checkCollision({
interval : 1e2,
maxHits : 8,
obj1 : { id : 1 },
obj2 : { id : 2 },
onHit : () => console.log("hit!"),
onMiss : () => console.log("miss!"),
onGameOver : () => {
console.log("game over!");
startGame({ name, firstTime : false });
}
})
});
counter.start();
};
startGame();
Useful Resources
How I separate logic from presentation?
This is my code that is unfortunately not working.
It is taking value from html (600) and then dividing it (var f = 600/20). It should start onclick button (which already has onclick function).
function enemy() {
var iFrequency = 2500;
var myInterval = 0;
var e = document.getElementById('stre').innerHTML;
var f = e / 20;
function startLoop() {
if (myInterval > 0) clearInterval(myInterval); // stop
myInterval = setInterval("doSomething()", iFrequency);
}
function doSomething() {
var label = document.getElementById('wallvalue');
label.innerHTML = parseInt(label.innerHTML) + f;
document.getElementById('wallvalue').style.color = 'red';
document.getElementById('resultr').innerHTML = f;
document.getElementById('resultr').style.color = 'red';
document.getElementById('resultrc').innerHTML = f;
document.getElementById('resultrc').style.color = 'red';
}
<BUTTON class="doit" onclick="damagec(); iFrequency+=1000; startLoop(); return false;"><b>FIGHT</b></BUTTON>
Looks like youve tried to do some inheritance, but your code is not working in any way. You may do this:
function enemy(name) { // a constructor for our enemy
this.name=name;
this.iFrequency = 2500;
this.myInterval = false;
this.el=document.createElement("div");
document.body.appendChild(this.el);
this.f = 1;
}
enemy.prototype={ //functions for all enemys
startLoop:function() {
if (this.myInterval) clearInterval(this.myInterval); // stop
this.myInterval = setInterval(this.doSomething.bind(this), this.iFrequency);
},
doSomething:function () {
this.el.innerHTML = this.name+":"+(++this.f);
this.el.style.color = 'red';
}
};
var Monster=new enemy("Monster");//create a new Enemey
var button=document.createElement("button");
button.textContent="Start Monster";
document.body.appendChild(button);
button.onclick=Monster.startLoop.bind(Monster);
I managed to get it done. Thank you all for the answers!
function enemy(){
var e=600;
var f=600/20;
document.getElementById('resultr').innerHTML = f;
document.getElementById('resultr').style.color = 'red';
document.getElementById('resultrc').innerHTML = f;
document.getElementById('resultrc').style.color = 'red';
var intervalID = window.setInterval(myCallback, 3000);
function myCallback() {
var label = document.getElementById('wallvalue');
label.innerHTML = parseInt(label.innerHTML) + f;
document.getElementById('wallvalue').style.color = 'red';
}
}