Pause and restart setTimeout in loop - javascript

I have put a delay in my loop by awaiting a function that returns a Promise with setTimeout(). I am able to start and reset the loop but would also like to be able to pause the loop somehow. After clicking the start button again, I would like to continue looping from the last looped element in the array.
$(document).ready(() => {
$('#btn-start').click(() => {
loopMoves();
});
$('#btn-pause').click(() => {
// ...
});
$('#btn-reset').click(() => {
clearTimeout(timer);
});
})
var moves = ['Nf3', 'd5', 'g3', 'g6', 'c4', 'dxc4'];
async function loopMoves() {
for(let i = 0; i < moves.length; i++){
console.log(moves[i]);
await delay(2000);
}
}
var timer;
function delay(ms) {
return new Promise((x) => {
timer = setTimeout(x, ms);
});
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<button id="btn-start">start</button>
<button id="btn-pause">pause</button>
<button id="btn-reset">reset</button>

Instead of using for loop this could be achieved by using setInterval() and a generator function.
Please refer below snippet for the working example:
$(document).ready(() => {
let loop = loopMoves();
$('#btn-start').click(() => {
console.log("****STARTED*****");
loop("PLAY");
});
$('#btn-pause').click(() => {
console.log("****PAUSED*****");
loop("PAUSE");
});
$('#btn-reset').click(() => {
console.log("****RESET*****");
loop("RESET");
});
})
const moves = ['Nf3', 'd5', 'g3', 'g6', 'c4', 'dxc4'];
function loopMoves() {
let moves = generateMoves();
let intervalId;
function startInterval(){
intervalId = setInterval(()=>{
const move = moves.next();
if(move.done)
clearInterval(intervalId);
else
console.log(move.value);
}, 2000);
}
return (state) => {
switch(state){
case "PLAY": startInterval(); break;
case "PAUSE": clearInterval(intervalId); break;
case "RESET": moves = generateMoves();
clearInterval(intervalId); break;
}
}
}
function* generateMoves(){
for(let i = 0; i < moves.length; i++){
yield moves[i];
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<button id="btn-start">start</button>
<button id="btn-pause">pause</button>
<button id="btn-reset">reset</button>

You might consider flipping things around into more of a time based "state machine".
When play is clicked, you start your timmer ("playing" state), at the end of the timmer you check the current state. If the state is still "playing" you grab the next move and display that and start the timmer again. If no interaction happens this repeats until all moves are done and the state goes to "stopped".
If someone clicks pause you go to "paused" state but leave the rest as is. When play is clicked again (going from "paused" to "playing") you just pick up where you were. When stop is clicked you reset everything. When play is clicked from the "stopped" state you set the move index to 0 and do the first move, start the timmer.
This does mean each update only happens every 2 seconds but that might be acceptable and it's a whole lot easier than the alternative of pausing the timmer etc.
const moves = ['Nf3', 'd5', 'g3', 'g6', 'c4', 'dxc4']; // These can be loaded however
let state = "stopped"; // The starting state
let currentMoveIndex = 0; // Used to track which move to grab for the next tick
$(document).ready(() => {
$('#btn-start').click(() => {
state = "playing";
tick();
});
$('#btn-pause').click(() => {
state = "paused";
});
$('#btn-reset').click(() => {
state = "stopped";
tick(); // Since we put the clean up inside the tick function we run it again to be sure it's executed in case it was paused
});
})
function tick() {
switch(state) {
case "playing":
if (currentMoveIndex + 1 === moves.length) {
break;
}
const move = moves[currentMoveIndex];
console.log(move); // Or whatever you wish to do with the move
currentMoveIndex += 1;
setTimeout(tick, 2000);
break;
case "paused":
// Do whatever you want based on entering the paused state,
// Maybe show some message saying "Paused"?
break;
case "stopped":
currentMoveIndex = 0;
break;
}
}
If you're using something like React or Angular you can wrap this into a Component/Controller to keep things together, or wrap it all into a Game function if you're using plain JavaScript

I have tried out these answers and both of them seem to work, thank you. I return to this question because I previously thought of something and would like to get some feedback on it. What if I would store the looped items in a new array and restart the loop from the position of the last stored item setting i = done.length (done being the array of looped items). When clicking the pause button clearTimeout() is called and when clicking the reset button, I clear out the done array after calling clearTimeout(), start still runs the function containing the loop.
I understand that I am not really pausing the timer but rather stopping it and restarting it from the last position in the array. Would this be considered 'bad practice' or could this also be an acceptable solution?
$(document).ready(() => {
$('#btn-start').click(() => {
loopMoves();
});
$('#btn-pause').click(() => {
clearTimeout(timer);
});
$('#btn-reset').click(() => {
clearTimeout(timer);
done = [];
});
})
var moves = ['Nf3', 'd5', 'g3', 'g6', 'c4', 'dxc4'];
var done = [];
async function loopMoves() {
for(let i = done.length; i < moves.length; i++){
console.log(moves[i]);
done.push(moves[i]);
await delay(2000);
}
}
var timer;
function delay(ms) {
return new Promise((x) => {
timer = setTimeout(x, ms);
});
}

Related

How to handle an interval play pause stop in javascript or nodejs?

I have a main file that acts like an API to manage the playing, resuming or a video.
main.js
const { mainModule } = require('process');
const { startVideo, pauseVideo, stopVideo } = require('./modules/video.js');
function init(payload) {
if(payload.type === 'play') {
handlePlayVideo(payload.data)
} else if(payload.type === 'pause') {
handlePauseVideo(payload.data);
} else if(payload.type === 'stop') {
handleStopVideo(payload.data);
}
}
function handlePlayVideo(data) {
startVideo(data).then(() => {
console.log('Video state: play');
});
}
function handlePauseVideo(data) {
pauseVideo().then(() => {
console.log('Video state: pause');
});
}
function handleStopVideo(data) {
stopVideo().then(() => {
console.log('Video state: stop');
});
}
I want to be able to use the data every second when the state(payload.type) is 'play'. That would start my interval, but I'm stuck on how to be able to pause it (then continue later from the same second), or stop it.
This is what I've tried:
video.js
module.exports = (function () {
let currentSecond = 0;
let videoIsPaused = false;
let videoIsStopped = false;
function startVideo(data) {
const videoTicker = setInterval(() => {
if(videoIsPaused || videoIsStopped) {
console.log('video is paused or stopped');
clearInterval(videoTicker);
if(videoIsStopped) currentSecond = 0;
}
parseVideo(data.someData, currentSecond);
if(currentSecond === 100) clearInterval(scenarioTicker);
currentSecond++;
}, 1000);
}
function pauseVideo() {
videoIsPaused = true;
}
function videoIsStopped() {
videoIsStopped = true;
}
function parseVideo(data) {
// do something (stream) the data every second, except when the video is paused, or stopped
}
})();
With this code, the startVideo method starts executing every second, but I cannot pause it.
Update:
The starting of the video has been started(tested) using cli (ex. node main.js), which started the execution of start video for let's say one minute. In the same time, I'm opening a second cmd window, where I try to call the pauseVideo of the first window. So I think that my issue is because of the separate window environments which acts like a different scope, and I assume that my calls cannot interact with each other in this way. Is there a better way of doing/testing this?
In your if statement for the paused/stopped condition, you're not stopping the execution of the rest of the function. Therefore, the parts below it, like increasing currentSecond will still be hit. Perhaps an early return is what you need:
if(videoIsPaused || videoIsStopped) {
console.log('video is paused or stopped');
clearInterval(videoTicker);
if(videoIsStopped) currentSecond = 0;
return;
}

What is "onkeyup" in gamepad api?

I'm experimenting a little with gamepad api. When a button is pressed, it gets "fired" all the time until the button is released. This is cool for something like running or shooting in a game, but problematic, when I want to navigate inside a menu (e.g. going 16 menu items down instead of one).
On a normal keyboard, I would use onkeyup/onkeydown to see, when a button press has ended. But on gamepad api, I didn't find something like that.
JS:
var gamepadPressCounter = 0;
function update() {
const gamepads = navigator.getGamepads();
if (gamepads[0].buttons[0].pressed === true) {
gamepadPressCounter++;
}
window.requestAnimationFrame(update);
}
So, do you know a way to get the gamepadPressCounter only one up per press and not infinite times?
You have to check it against whether it was pressed last time you checked. It doesn't fire events, it is stateful. You are directly checking if the button is pressed or not.
Make a variable to store the state of the button last time you checked and compare it to see if it has changed from last time.
const anyButtonPressedChangeListener = {
listeners: [],
addListener(cb) {
this.listeners.push(cb);
},
signal(state) {
for (const cb of this.listeners) {
cb(state);
}
}
}
let buttonstate = false;
const loop = () => {
const gamepads = navigator.getGamepads();
const gamepad = gamepads[0];
if (gamepad) {
const statenow = gamepad.buttons.some(btn => btn.pressed);
if (buttonstate !== statenow) {
buttonstate = statenow;
anyButtonPressedChangeListener.signal(statenow);
}
} else {
console.log("Not connected");
}
setTimeout(loop, 1000);
}
anyButtonPressedChangeListener.addListener((buttonState) => {
console.log(`Any button pressed changed to ${buttonState}`);
});
loop();

What am I doing bad with localStorage?

I am doing a time-to-click score table but first I have to localstorage each time I get in a game but I dont know how I have been trying everything but it still not working, I need to finish this fast, and I need help... otherwise I would try everyday to resolve this alone because I know that's the way to learn.. When I press the finished button It says that times.push() is not a function.
let times = Array.from(
{ length: 3 }
)
let interval2;
// Timer CountUp
const timerCountUp = () => {
let times = 0;
let current = times;
interval2 = setInterval(() => {
times = current++
saveTimes(times)
return times
},1000);
}
// Saves the times to localStorage
const saveTimes = (times) => {
localStorage.setItem('times', JSON.stringify(times))
}
// Read existing notes from localStorage
const getSavedNotes = () => {
const timesJSON = localStorage.getItem('times')
try {
return timesJSON ? JSON.parse(timesJSON) : []
} catch (e) {
return []
}
}
//Button which starts the countUp
start.addEventListener('click', () => {
timerCountUp();
})
// Button which stops the countUp
document.querySelector('#start_button').addEventListener('click', (e) => {
console.log('click');
times = getSavedNotes()
times.push({
score: interval2
})
if (interval) {
clearInterval(interval);
clearInterval(interval2);
}
})
You probably need to change the line times = JSON.parse(localStorage.times || []) to times = JSON.parse(localStorage.times) || [] this makes sure that if the parsing fails it will still be an array.
Even better is wrap them with try-catch just in case if your JSON string is corrupted and check if the time is an array in the finally if not set it to empty array.

Loop through array at certain speed in Javascript

I'm new here so spare me please.
I'm working on a little hobby project of mine where an application gathers telemetry which is recorded and saved at 60Hz (so 60 index in an array for each second). This is always the case.
I then want to 'play through' this array at the regular speed as if you're playing a video. But instead of seeing a video I want to show the current data from each index that you're at on the frontend.
The structure of the array is not made yet, but I assume I will just store and load a JSON file somewhere in the database. This then should be played through in my frontend. I've used Angular (6) to build that, so it would be great if it could be mixed with Angular (to keep track of the progress of which index you are now, and bind the values of the current index to the frontend).
Would it be easy to just use 0-1-2-etc for indexx, or maybe something like timestamps?
Any suggestions are welcome. The most important thing here is to keep it quick so you don't need a strong rig to play this.
Thanks in advance!
You need to use setInterval function in which you will iterate over the array according to your frequency. So if your frequency is 60hz that means you want to iterate to next element in array after every 1000 / 60 milliseconds
var data = [1, 2, 3, 4, 5]
var currentIndex = 0;
var interval = 1000 / 60
var id = setInterval(function() {
// do your thing
if(currentIndex == (data.length-1)) {
clearInterval(id)
} else {
currentIndex++
}
}, interval)
This is not a particularly iterating over array but rather doing some action after interval of time and then moving to next item and when you are done with array this will clear the interval. Perhaps linked list will be more helpful than array here
You can make yourself a simple runner for such things. It's basically a game engine and you need a game loop :)
Here's a naive example. Skipping most of error checking and validation here for brewity.
class TelemetryPlayer {
constructor(items, render, interval) {
// setup local data.
this.interval = interval;
this.items = items;
this.render = render;
this.startLoop();
}
// Loop simply initializes before the first run, and then runs the loop.
// Other places do the actual work.
startLoop() {
this._renderInProgress = false;
this._currentIndex = 0;
// save the interval reference if you wanna pause/reset later on.
this._interval = setInterval(this.doWork.bind(this), this.interval);
}
// here we perform the actual render.
doWork() {
if (this._renderInProgress) {
// previous render has not completed yet.
console.log('skip');
return;
}
console.log('Tick');
this._renderInProgress = true;
const item = this.items[this._currentIndex];
console.log('Tick');
// now, call your renderer, and update stuff when complete.
this.render(item)
.then(() => {
// Or this can be a callback or similar.
this._renderInProgress = false;
// Ready next item. Do not go out of array bounds.
this._currentIndex++;
if (this._currentIndex === this.items.length) {
this._currentIndex = 0;
}
});
}
// You can then add fun things like skip, pause, reset etc.
skip(item) {
if (item < 0 || item > this.items.length) {
return;
}
// pause first
this.pause();
this._currentIndex = item;
this.unpause();
}
//
reset() {
this.skip(0);
}
//
pause() {
this._interval = clearInterval(this._interval);
}
unpause() {
if (!this._interval) {
this._interval = setInterval(this.doWork.bind(this), this.interval);
}
}
// you can even add items later
addItem(item) {
this.items.push(item);
}
// or replace them.
replaceItem(item, index) {
this.items[index] = item;
// show the new item right away.
this.skip(index);
}
// or add an item to be played just once.
playOnce(item) {
this.pause();
this.render(item);
this.unpause();
}
}
Now here's an example usage. You can copy the code (both class above and the code block bellow) and paste it into console right here on StackOverflow to see it in work. You likely wanna do other things, but you'll get the gist.
let items = [ 100, 200, 300, 50, 100, 200, 300, 250 ];
const timeline = document.createElement('ul');
// maybe better do this by adding class and external stylesheet
timeline.setAttribute('style', 'padding: 15px; border: 1px solid gray; list-style-type: none; height: 500px; width: 100%; position: absolute; top: 0;overflow-y: scroll;')
document.body.appendChild(timeline);
let render = function(item) {
return new Promise(function (resolve) {
// run something (in) expensive with item now.
const li = document.createElement('li');
// again, better do this with class.
li.setAttribute('style', `display: inline-block; width: 25px; margin: 5px; background-color: #c1c1c1; height: ${item}px;`);
timeline.appendChild(li);
li.scrollIntoView();
// return when done.
resolve();
});
}
const player = new TelemetryPlayer(items, render, 1500);
// now you can do things like speed up etc.
// now you can do things like speed up etc.
function speedUp() {
// speedup 3 seconds after "playback" starts.
return new Promise((resolve) => setTimeout(() => {
player.pause();
player.interval = 600;
player.unpause();
resolve();
}, 3000));
}
function playOnce() {
// add item once, but not in the array we loop around in
return new Promise((resolve) => setTimeout(() => {
player.playOnce(1000);
resolve();
}, 3000));
}
// or add a few items that will be repeated.
function addItems() {
// add a super very high item 3 seconds after function call.
return new Promise((resolve) => setTimeout(() => {
player.pause();
player.addItem(400);
player.addItem(430);
player.addItem(420);
player.unpause();
// now rewind to this block. I am off by one, likely.
player.skipTo(player.items.length - 3);
resolve();
}, 5000))
}
speedUp()
.then(playOnce)
.then(addItems);

Reset timeout on event with RxJS

I'm experimenting with RxJS (with the JQuery extension) and I'm trying to solve the following use case:
Given that I have two buttons (A & B) I'd like to print a message if a certain "secret combination" is clicked within a given timeframe. For example the "secret combination" could be to click "ABBABA" within 5 seconds. If the combination is not entered within 5 seconds a timeout message should be displayed. This is what I currently have:
var secretCombination = "ABBABA";
var buttonA = $("#button-a").clickAsObservable().map(function () { return "A"; });
var buttonB = $("#button-b").clickAsObservable().map(function () { return "B"; });
var bothButtons = Rx.Observable.merge(buttonA, buttonB);
var outputDiv = $("#output");
bothButtons.do(function (buttonName) {
outputDiv.append(buttonName);
}).bufferWithTimeOrCount(5000, 6).map(function (combination) {
return combination.reduce(function (combination, buttonName) {
return combination + buttonName;
}, "");
}).map(function (combination) {
return combination === secretCombination;
}).subscribe(function (successfulCombination) {
if (successfulCombination) {
outputDiv.html("Combination unlocked!");
} else {
outputDiv.html("You're not fast enough, try again!");
}
});
While this works fairly well it's not exactly what I want. I need the bufferWithTimeOrCount to be reset when button A is pressed for the first time in a new timeframe. What I'm looking for is that as soon as the secret combination is pressed (ABBABA) I'd like "Combination unlocked!" to be shown (I don't want to wait for the time window to be expired).
Throttle is the typical operator for the delaying with reactive resetting effect you want.
Here's how you can use throttle in combination with scan to gather the combination inputted before the 5 seconds of silence:
var evaluationStream = bothButtons
.merge(bothButtons.throttle(5000).map(function(){return "reset";})) // (2) and (3)
.scan(function(acc, x) { // (1)
if (x === "reset") return "";
var newAcc = acc + x;
if (newAcc.length > secretCombination.length) {
return newAcc.substr(newAcc.length - secretCombination.length);
}
else {
return newAcc;
}
})
.map(function(combination) {
return combination === secretCombination;
});
var wrongStream = evaluationStream
.throttle(5000)
.filter(function(result) { return result === false; });
var correctStream = evaluationStream
.filter(function(result) { return result === true; });
wrongStream.subscribe(function() {
outputDiv.html("Too slow or wrong!");
});
correctStream.subscribe(function() {
outputDiv.html("Combination unlocked!");
});
(1) We scan to concatenate the input characters. (2) Throttle waits for 5 seconds of event silence and emits the last event before that silence. In other words, it's similar to delay, except it resets the inner timer when a new event is seen on the source Observable. We need to reset the scan's concatenation (1), so we just map the same throttled Observable to "reset" flags (3), which the scan will interpret as clearing the accumulator (acc).
And here's a JSFiddle.

Categories

Resources