How can I cancel setInterval running with global variable? - javascript

I am trying to clear/stop setInterval from running, and have made changes to my approach based on other questions/answers I've found, but still can't get setInterval to stop running.
What I have here, is a Mutation Observer which detects a class change, and when that occurs, if two conditions are true (the presence of other classes), then some cursor tracking executes. All this works great.
When one of the two classes, .run-preview, is removed I then want to stop setInterval from running, as seen in the else if. I also have a commented line there where I was verifying that the else if is working, and it is.
I have a global variable intervalCursor = setInterval(move,1000/60);, and then am using that variable to later clearInterval(intervalCursor);.
const runPreview = document.querySelector('.projects');
new MutationObserver((mutations) => {
if (mutations[0].attributeName === 'class') {
console.log('Project list class changed');
if ($('html').hasClass('no-touchevents') && ($('.projects').hasClass('run-preview'))) {
var mouseX=window.innerWidth/2,
mouseY=window.innerHeight/2;
var intervalCursor = setInterval(move,1000/60);
var projectPreview = {
el:$('.image-container'),
x:window.innerWidth/2,
y:window.innerHeight/2,
w:300,
h:300,
update:function() {
l = this.x-this.w/2;
t = this.y-this.h/2;
this.el.css({
'transform':
'translate3d('+l+'px,'+t+'px, 0)' });
//console.log("transform");
}
}
$(window).mousemove (function(e){
mouseX = e.clientX;
mouseY = e.clientY;
//console.log("mousemove");
})
function move(){
projectPreview.x = lerp (projectPreview.x, mouseX, 0.1);
projectPreview.y = lerp (projectPreview.y, mouseY, 0.1);
projectPreview.update()
console.log("move");
}
function lerp (start, end, amt){
return (1-amt)*start+amt*end
}
} else if (!$('.projects').hasClass('run-preview')) {
clearInterval(intervalCursor);
return;
//$('#content-container').addClass('test');
}
}
})
.observe(runPreview, { attributes: true });
Am I doing something wrong?

Thanks to Ouroborus, I was able to solve this by creating the variable separately from the MutationObserver, and then using it to run setInterval(move,1000/60); later. Thus allowing setInterval to be cleared/stopped when I need to.
var intervalCursor
const runPreview = document.querySelector('.projects');
new MutationObserver((mutations) => {
if (mutations[0].attributeName === 'class') {
console.log('Project list class changed');
if ($('html').hasClass('no-touchevents') && ($('.projects').hasClass('run-preview'))) {
var mouseX=window.innerWidth/2,
mouseY=window.innerHeight/2;
var projectPreview = {
el:$('.image-container'),
x:window.innerWidth/2,
y:window.innerHeight/2,
w:300,
h:300,
update:function() {
l = this.x-this.w/2;
t = this.y-this.h/2;
this.el.css({
'transform':
'translate3d('+l+'px,'+t+'px, 0)' });
//console.log("transform");
}
}
$(window).mousemove (function(e){
mouseX = e.clientX;
mouseY = e.clientY;
//console.log("mousemove");
})
intervalCursor = setInterval(move,1000/60);
function move(){
projectPreview.x = lerp (projectPreview.x, mouseX, 0.1);
projectPreview.y = lerp (projectPreview.y, mouseY, 0.1);
projectPreview.update()
console.log("move");
}
function lerp (start, end, amt){
return (1-amt)*start+amt*end
}
} else if (!$('.projects').hasClass('run-preview')) {
clearInterval(intervalCursor);
//$('#content-container').addClass('test');
}
}
})
.observe(runPreview, { attributes: true });

Related

Remove Event Listeners that need to Receive Multiple Arguments JS

I built a tab that a user can close using touch events and when the tab closes I want to be able to remove event listeners. This is a little bit tricky because in my actual code there is a modal and there is a part in that dynamically inserted content that the touch events are attached to.
So if i have the below code
const tab = document.querySelector('.tab')
function handleMove(e, tab) {
e.preventDefault()
tab.style....
}
// I add the event listener like this
// but then I can't remove it
tab.addEventListener('touchmove', e => {
handleMove(e, tab)
})
Below is more realistic with what I am dealing with.
const swipeTab = document.querySelector('.swipeable-tab')
let y1, timeStart, timeEnd
function closeStart(e) {
e.preventDefault()
let touchLocation = e.targetTouches[0]
y1 = touchLocation.clientY
console.log({ y1 })
timeStart = e.timeStamp
}
function closeMove(e, swipeTab) {
e.preventDefault()
swipeTab.style.transform = ''
let touchLocation = e.touches[0]
let yLocation = touchLocation.clientY
if (yLocation > swipeTab.clientHeight + y1) {
yLocation = swipeTab.clientHeight + y1
}
swipeTab.style.transition = ''
let marker = yLocation - y1
console.log({ marker })
if (marker < 0) {
marker = 0
}
swipeTab.style.transform = `translate3d(0, ${marker}px, 0)`
}
function closeEnd(e, swipeTab) {
e.preventDefault()
let touchLocation = e.changedTouches[0];
let y2 = touchLocation.clientY;
let yDiff = y2 - y1;
console.log({ yDiff })
timeEnd = e.timeStamp;
timeDiff = timeEnd - timeStart;
console.log({ y2 })
console.log({ timeDiff })
if (yDiff > swipeTab.clientHeight/3 || timeDiff < 50) {
closeTab(swipeTab)
} else {
openTab(swipeTab)
}
}
function openTab(swipeTab) {
swipeTab.style.transition = `all 0.2s ease-in-out`
swipeTab.style.transform = `translate3d(0, 0%, 0)`
addCloseEventListeners(swipeTab)
}
/**
* I am trying to come up with something similar to this
*/
function closeTab(swipeTab) {
swipeTab.style.transition = `all 0.2s ease-in-out`
swipeTab.style.transform = `translate3d(0, 100%, 0)`
removeCloseEventListeners(tab)
}
function removeCloseEventListeners(swipeTab) {
swipeTab.removeEventListener('touchstart', closeStart);
swipeTab.removeEventListener('touchmove', closeMove);
swipeTab.removeEventListener('touchend', closeEnd);
}
/**
* when open(swipeTab) is called
* then the event listeners are added for closing the tab
*/
function addCloseEventListeners(swipeTab) {
swipeTab.addEventListener('touchstart', e => {
closeStart(e)
})
swipeTab.addEventListener('touchmove', e => {
closeMove(e, swipeTab)
})
swipeTab.addEventListener('touchend', e => {
closeEnd(e, swipeTab)
})
}
/**
* this is where it starts
*/
open(swipeTab)
Make more variables.
function removeCloseEventListeners(swipeTab) {
swipeTab.removeEventListener("touchstart", swipeTouchStart);
swipeTab.removeEventListener("touchmove", swipeTouchMove);
swipeTab.removeEventListener("touchend", swipeTouchEnd);
}
const swipeTouchStart = (e) => closeStart(e);
const swipeTouchMove = (e) => closeMove(e, swipeTab);
const swipeTouchEnd = (e) => closeEnd(e, swipeTab);
function addCloseEventListeners(swipeTab) {
swipeTab.addEventListener("touchstart", swipeTouchStart);
swipeTab.addEventListener("touchmove", swipeTouchMove);
swipeTab.addEventListener("touchend", swipeTouchEnd);
}
Also, you don't need to make your functions take swipeTab as a parameter:
function addCloseEventListeners() {
since swipeTab is already a global variable in the script.
I would probably create a helper function that adds a listener and returns a callback holding a removeEventListener() with the same signature that it was added with. You can then store these callbacks to be used in the removeCloseEventListeners() function, here using a global array and setting it's length to 0 after use.
This will redeclare the callbacks for each event with the passed swipeTab as argument and as the element the events will be attached to. It also avoids the repetition in your current code allowing you to define the events once in addCloseEventListeners() without having to also add/edit them in the close function.
function addEventListenerWithAnonymousCallback(element, event, cb, options = {}) {
element.addEventListener(event, cb, options);
return () => element.removeEventListener(event, cb, options);
}
const closeListeners = [];
function removeCloseEventListeners() {
for (const removeListener of closeListeners) {
removeListener();
}
closeListeners.length = 0;
}
function addCloseEventListeners(swipeTab) {
const listeners = [
["touchstart", (e) => closeStart(e)]
["touchmove", (e) => closeMove(e, swipeTab)],
["touchend", (e) => closeEnd(e, swipeTab)],
];
for (const [event, cb] of listeners) {
const removeListener = addEventListenerWithAnonymousCallback(swipeTab, event, cb);
closeListeners.push(removeListener);
}
}

Return after clearInterval

I have a code that runs setInterval in a function. What I try to achive is to exit from that function after clearInterval occured. I set up a condition to check wheter the interval is cleared. I'm struggling to find a method to return from the main Start() function based on the current state of the code.
Code:
function Start() {
var elementX = document.getElementById('moveMeX')
var elementY = document.getElementById('moveMeY')
elementY.style.top = "0px"
var posX = 0
startPosX = 0
var posY = 0
startPosY = 0
var speed = 1
var xIsMoving = true
var yIsMoving = true
var myIntervalX = setInterval(function() {
if(startPosX == 0) {
posX+=speed
elementX.style.left = posX + 'px'
if(posX==100) {
startPosX = 100
}
}
else if (startPosX==100) {
posX-=speed
elementX.style.left = posX + 'px'
if(posX==0) {
startPosX = 0
}
}
}, 10);
function stopX() {
clearInterval(myIntervalX);
xIsMoving = false
}
elementX.addEventListener("mousedown", stopX);
elementX.addEventListener("mousedown", startY);
function startY() {
var myIntervalY = setInterval(function() {
if(startPosY == 0) {
posY+=speed
elementY.style.top = posY + 'px'
if(posY==100) {
startPosY = 100
}
}
else if (startPosY==100) {
posY-=speed
elementY.style.top = posY + 'px'
if(posY==0) {
startPosY = 0
}
}
}, 10);
function stopY() {
elementY.style.zIndex = "-1";
clearInterval(myIntervalY);
yIsMoving = false
}
elementY.addEventListener("mousedown", stopY);
}
if (xIsMoving === false && yIsMoving === false) {
console.log('stopped')
stopped = true
}
}
Start()
Codepen link: https://codepen.io/silentstorm902/pen/podNxVy
The part you state you have a problem with,
if (xIsMoving === false && yIsMoving === false) {
console.log('stopped')
stopped = true
}
currently only runs once: when Start() is first called. At that time, both conditions are false because the variables have their initial declared values of true.
You could create a new setInterval to run this if statement every so often, but it seems unnecessary because your current stopY function is really the place where you know that both xIsMoving and yIsMoving are both false -- because stopY only runs after stopX has run. So just move your statement of stopped=true at the end of the stopY function:
function stopY() {
elementY.style.zIndex = "-1";
clearInterval(myIntervalY);
yIsMoving = false;
console.log("stopped");
stopped = true;
}

Dynamically creating oscillators causes memory issues in p5 Sound

Within my code (the code below is a very shortened version), I am creating oscillators (https://p5js.org/reference/#/p5.Oscillator) in draw. Eventually the audio starts crackling and being all weird, and I assume this is because the oscillators aren't being garbage collected? When looking at the Memory tab in Inspect Element, it continously goes up, and the audio goes crackly eventually.
I tried to dereference the oscillators using the two functions in my disconnect variable/function. This sets the oscillator to null after a specific amount of time I hope. I wasn't sure how to set a reference argument to null.
Maybe the p5 sound library still keeps references for them?
function createOsc(grid) {
let osc = new p5.Oscillator();
if (grids[grid].type == 1) {
osc.setType("triangle");
} else if (grids[grid].type == 2) {
osc.setType("sawtooth");
} else if (grids[grid].type == 3) {
osc.setType("square");
}
return osc;
}
function createEnv(grid) {
let env = new p5.Envelope();
env.set(grids[grid].ADSR.attack, grids[grid].vol, grids[grid].ADSR.decay, grids[grid].ADSR.sustain * grids[grid].vol, grids[grid].ADSR.release, 0);
return env;
}
let disconnect = {
list: [],
osc: function(osc, time) {
this.list.push([osc, time, 0]);
},
update: function() {
for (i = 0; i < this.list.length; i++) {
this.list[i][2] += 1 / 60.0;
if (this.list[i][2] >= this.list[i][1]) {
this.list[i][0].a.disconnect();
this.list[i][0].a = null;
this.list.splice(i, 1);
i += -1;
}
}
}
}
function draw() {
let osc = createOsc(0);
let env = createEnv(0);
osc.start();
osc.freq(420);
env.triggerAttack(osc);
env.triggerRelease(osc, env.aTime + env.dTime);
osc.stop(env.aTime + env.dTime + env.rTime);
disconnect.osc({a: osc}, env.aTime + env.dTime + env.rTime);
}
I attempted to use the PolySynth (https://p5js.org/reference/#/p5.PolySynth) object, but I couldn't figure out how to change the waveforms or anything due to lack of clear documentation.
Edit:
This new standalone code produces the same crackling audio after a while. I am also now using Millis() as suggested. Same issue with memory constantly increasing.
let disconnect = {
list: [],
osc: function(osc, time) {
this.list.push([osc, time, millis()]);
},
update: function() {
for (i = 0; i < this.list.length; i++) {
if (millis() - this.list[i][2] >= this.list[i][1]) {
this.list[i][0].a.disconnect();
this.list[i][0].a = null;
this.list.splice(i, 1);
i += -1;
}
}
}
}
function createOsc() {
let osc = new p5.Oscillator();
osc.setType("triangle");
return osc;
}
function createEnv() {
let env = new p5.Envelope();
env.set(0.1, 1, 0.1, 0.8, 0.1, 0);
return env;
}
function setup() {
frameRate(60);
}
function draw() {
disconnect.update();
print(disconnect.list.length);
if (frameCount % 2 == 0) {
let osc = createOsc();
let env = createEnv();
osc.start();
osc.freq(420);
env.triggerAttack(osc);
env.triggerRelease(osc, 0.2);
osc.stop(0.3);
disconnect.osc({a: osc}, 300);
}
}

context.clearRect() doesn't work as expected

So I have a maze here and the problem arises when user changes rowNum/colNum in the maze.
When user changes either of them, it calls
ctx.clearRect(0, 0, canvas.width, canvas.height).
This clears up the whole canvas but when user again starts moving the player, the player from previously cleared up canvas appears up in the this canvas. see here.
clearRect() clears up the canvas but I don't know why the player is still left there to interact with.
codesandbox link
Here's a shortened description of what my code does to draw the player-
main.js
let customGrid = document.querySelector("#rows,#columns");
customGrid.forEach(elem => elem.addEventListener("change", e => {
// detect changed element
if (e.target.id === "row")
customGrid[0].value = parseInt(e.target.value);
else
customGrid[1].value = parseInt(e.target.value);
ctx.clearRect(0, 0, canvas.width, canvas.width);
// setting myMaze to new instance of Maze
myMaze = new Maze(ctx, 600, customGrid[0].value, customGrid[1].value);
myMaze.setup();
myMaze.drawMap();
})
);
Next, myMaze.drawMap() contains-
//set player
this.player = new Player(this.ctx, this.goal, this.cellWidth, this.cellHeight);
this.player.setPlayer(this);
From here setPlayer calls-
setPlayer(myMaze) {
....
this.drawPlayer();
this.listenMoves(myMaze);
}
listenMoves(myMaze) {
window.addEventListener("keydown", function handler(e) {
myMaze.player.move(e.keyCode, myMaze);
let reachedCol = myMaze.player.colNum === myMaze.goal.colNum ? true : false;
let reachedRow = myMaze.player.rowNum === myMaze.goal.rowNum ? true : false;
if (reachedRow && reachedCol) {
alert("reached!");
window.removeEventListener("keydown", handler);
}
});
}
move(input, myMaze) {
let current = myMaze.grid[this.rowNum][this.colNum];
let walls = current.walls;
switch(input) {
case 37:
if(!walls.leftWall) {
this.colNum -= 1;
} break;
case 38:
if(!walls.topWall) {
this.rowNum -= 1;
} break;
case 39:
if(!walls.rightWall) {
this.colNum += 1;
} break;
case 40:
if(!walls.bottomWall) {
this.rowNum += 1;
}
}
this.ctx.clearRect(current.xCord, current.yCord, current.width, current.height);
current.drawCell();
this.drawPlayer();
}
I would move the event listener up to main.js. Since Maze has the reference to Player, it should be possible. I am assuming you have a global variable of Maze (myMaze).
let myMaze;
let reched = false;
window.addEventListener("keydown", function handler(e) {
if (!myMaze || reched) {
return;
}
myMaze.player.move(e.keyCode, myMaze);
myMaze.player.handleMove();
let reachedCol = myMaze.player.colNum === myMaze.goal.colNum ? true : false;
let reachedRow = myMaze.player.rowNum === myMaze.goal.rowNum ? true : false;
if (reachedRow && reachedCol) {
alert("reached!");
reched = true;
}
});
If you want to keep the event handler as Player's method, you could do something like below. And call unListenMoves() when the grid size changes.
class Player {
constructor(ctx, goal, cellWidth, cellHeight, myMaze) {
// keep the Maze instance as a Player's prop for the later use
this.myMaze = myMaze;
// we need to bind this here, not in listenMoves
this.handleMove = this.handleMove.bind(this);
}
listenMoves() {
window.addEventListener("keydown", this.handleMove);
}
unListenMoves() {
window.removeEventListener("keydown", this.handleMove);
}
handleMove(e) {
const myMaze = this.myMaze;
myMaze.player.move(e.keyCode, myMaze);
let reachedCol = myMaze.player.colNum === myMaze.goal.colNum ? true : false;
let reachedRow = myMaze.player.rowNum === myMaze.goal.rowNum ? true : false;
if (reachedRow && reachedCol) {
alert("reached!");
window.removeEventListener("keydown", this.handleMove);
}
}
}

Make onkeydown events occur only once for specific keys

I am creating a simple game with functions to move a player forward,turn left,turn right, or turn around. I want the key presses to fire the specific corresponding function only once. I found this helpful code that fires the event only once, but I cannot seem to figure out how to specify certain key presses within it.
var shouldHandleKeyDown = true;
document.onkeydown = function(){
if (!shouldHandleKeyDown) return;
shouldHandleKeyDown = false;
// HANDLE KEY DOWN HERE
}
document.onkeyup = function(){
shouldHandleKeyDown = true;
}
})();
What I am trying to make happen is:
User presses up?
Move forward function occurs ONCE (even if the up key is held)
User presses left? Turn left function occurs once etc....
Thank you all for any help with this.
EDIT FOR CLARIFICATION:
I am trying to build a first-person dungeon crawler in the style of old titles like Wizardry for the NES. Basically I want each press of the forward button to move the player forward one frame. If they press left then the player turns left one frame, etc. If you are familiar with the games you will get what I mean.
I would ignore onkeydown events and just use onkeyup. That way the event is only fired once the user lifts their finger, ensuring just one movement per key press.
To determine which key was pressed inside your event handler, pass the event to your function. Here is a link to the values of the keys, so you could do something like:
document.onkeyup = function(event) {
var keycode = event.keyCode;
if (keycode === 38) {
moveUp();
}
}
There is no single good way to tackle this problem, it mostly depends on what you'll do with these key inputs.
For instance, if it is to control an always updating animation, then simple semaphores in a dictionary with {left: bool, right: bool, up: bool, down: bool} sets to true in keydown and to false in keyup, and that gets checked at every frame is enough.
const directions = {
up: false,
right: false,
down: false,
left: false
};
onkeydown = e => {
const key = e.key.replace('Arrow', '').toLowerCase();
if(key in directions) {
e.preventDefault();
directions[key] = true;
}
};
onkeyup = e => {
const key = e.key.replace('Arrow', '').toLowerCase();
if(key in directions) {
e.preventDefault();
directions[key] = false;
}
};
const w = canvas.width = innerWidth;
const h = canvas.height = innerHeight;
const ctx = canvas.getContext('2d');
const character = {
x: w/2,
y: h/2,
update: function() {
const speed = 2;
// this is where we will check our keyboard's status
// we do it every frame
let dir_x = 0,
dir_y = 0;
dir_x += directions.left ? -1 : 0;
dir_x += directions.right ? 1 : 0;
dir_y += directions.up ? -1 : 0;
dir_y += directions.down ? 1 : 0;
this.x += dir_x * speed;
this.y += dir_y * speed;
},
draw: function() {
ctx.fillRect(this.x - 20, this.y-20, 40, 40);
}
};
function anim() {
character.update();
ctx.clearRect(0,0,w, h);
character.draw();
requestAnimationFrame(anim);
}
anim();
:root,body{margin:0px}
<canvas id="canvas"></canvas>
However if you are not keeping a continuous animation loop and really need this event to fire at first action, then in the keydown handler you may need to add a logic
...
if(key in directions) {
e.preventDefault();
const changed = directions[key] !== true;
directions[key] = true;
if(changed) {
triggerChange(key);
}
}
Where triggerChange(key) would be a function ready to trigger your solo action.
const directions = {
up: false,
right: false,
down: false,
left: false
};
function triggerChange(dir) {
character.update();
}
onkeydown = e => {
const key = e.key.replace('Arrow', '').toLowerCase();
if (key in directions) {
e.preventDefault();
const changed = directions[key] !== true;
directions[key] = true;
if (changed) {
triggerChange(key);
}
}
};
onkeyup = e => {
const key = e.key.replace('Arrow', '').toLowerCase();
if (key in directions) {
e.preventDefault();
directions[key] = false;
}
};
const w = canvas.width = innerWidth;
const h = canvas.height = innerHeight;
const ctx = canvas.getContext('2d');
const character = {
x: w / 2,
y: h / 2,
update: function() {
const speed = 5;
// this is where we will check our keyboard's status
// we do it every frame
let dir_x = 0,
dir_y = 0;
dir_x += directions.left ? -1 : 0;
dir_x += directions.right ? 1 : 0;
dir_y += directions.up ? -1 : 0;
dir_y += directions.down ? 1 : 0;
this.x += dir_x * speed;
this.y += dir_y * speed;
},
draw: function() {
ctx.fillRect(this.x - 20, this.y - 20, 40, 40);
}
};
function anim() {
// it is now keydown that is responsible for character.update()
ctx.clearRect(0, 0, w, h);
character.draw();
requestAnimationFrame(anim);
}
anim();
:root,body{margin:0}
<canvas id="canvas"></canvas>
Change function() to function(e) and pressed key is in e.key
(after run snippet click on white rectangle below run button and press keys)
var shouldHandleKeyDown = true;
document.onkeydown = function(e) {
if (!shouldHandleKeyDown) return;
shouldHandleKeyDown = false;
console.log(e.key);
// HANDLE KEY DOWN HERE
}
document.onkeyup = function(){
shouldHandleKeyDown = true;
}

Categories

Resources