Dynamically modifying speed in a Javascript setInterval function

Viewed 145

I have a react application that needs to display some text with different interval between the sentences, one at a time.

So what I did is to put all the sentences in an array and create a setInterval function:

  startShowingText() {
    let i = 0

    this.setState({
      timer: window.setInterval(() => {
        this.setState({
          sentence: SENTENCES[i]
        })

        i += 1
      }, this.state.startingDuration)
    })
  }

Unfortunately this approach has many downside:

1) It needs a check to terminate;

2) I can't modify the time dynamically, so once this.state.startingDuration is set, that's going to be the speed for all the sentences...

3) For some reasons, if I assign the window.setInterval function to a variable outside the state, that piece of code is not executed:

  startShowingText() {
    let i = 0
    console.log('here')
    const timer = window.setInterval(() => {
      this.setState({
        sentence: SENTENCES[i]
      })

      i += 1
    }, this.state.startingDuration)
    console.log('there')
  }

This code was just printing:

here

there

Without triggering the actual timer.

How can I improve the code and make the interval dynamically adjustable?

3 Answers

One approach you could use borrows from the game development world where they create "game loops" that run at a certain tick interval. Within the loop, you could determine if it is time to output the sentence or not based on the current loop iteration.

A rough example:

const sentences = {
    "My first sentence.": 1,
    "My second sentence.": 20, 
    "My third sentence.": 25, 
    "My fourth sentence.": 40
};
const ticks = 100;
const maxLoops = 100;

const i = setInterval(writeLoop, ticks);

let loopCounter = 0;
function writeLoop() {
 
    if (loopCounter > maxLoops) {
        clearInterval(i);
    }
  
    for (sentence in sentences) {
        let targetLoop = sentences[sentence];
        if (loopCounter == targetLoop) {
         document.write('<div>'+sentence+'</div>');
        }
    }
  
    loopCounter = loopCounter + 1;
}

You can use setTimeout and recursive function like below;

class App extends React.Component {
  sentences = ["sentence1", "sentence2", "sentence3"];
  delayTime = [1000, 500, 3000];
  
  constructor(props) {
    super(props);

    this.state = {
       selected: 0,
    }
  }
  componentDidMount() {
    this.showSentence();
  }
  showSentence() {
    const { selected } = this.state;
    setTimeout(() => {
      this.setState({ selected: (selected + 1) % 3});
      this.showSentence();
    }, this.delayTime[selected])
  }
  render() {
    return (
       <div>{this.sentences[this.state.selected]}</div>
    );
  }
}

ReactDOM.render(<App/>, document.getElementById('app'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>

<div id='app'/>

Result: sentence1 -1s-> sentence2 -0.5s-> sentence3 -3s-> sentence1 again

You can recursively call setTimeout and update the speed based on the current dialog block.

The speed for each dialog is letters typed per second.

var dialog = [{
  text : "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
  speed : 120
}, {
  text : "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
  speed : 60
}, {
  text : "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
  speed : 90
}, {
  text : "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
  speed : 30
}];

let estimatedTime = dialog.reduce((d, e) => d + ((1000 / e.speed) * e.text.length) / 1000, 0);
console.log(`Estimated time: ${estimatedTime.toFixed(3)} seconds`);

var startTime = new Date().getTime();
typeDialog(dialog, document.getElementById('lorem'), () => {
  let endTime = new Date().getTime();
  let duration = (endTime - startTime) / 1000;
  let latency = duration - estimatedTime;
  console.log(`Task took: ${duration.toFixed(3)} seconds`);
  console.log(`Latency: ${latency.toFixed(3)} seconds`);
});

function typeDialog(dialog, targetEl, completeFn) {
  if (dialog.length > 0) {
    let index = 0, speed = 1000 / dialog[index].speed,
      currentParagraph = document.createElement('p');
    targetEl.appendChild(currentParagraph);
    typeCharacter(index, 0, speed);
    function typeCharacter(index, letter, speed) {
      if (letter > dialog[index].text.length) {
        index++; letter = 0;
        if (index < dialog.length) {
          speed = 1000 / dialog[index].speed;
          currentParagraph = document.createElement('p');
          targetEl.appendChild(currentParagraph);
        }
      }
      if (index < dialog.length) {
        setTimeout(() => {
          currentParagraph.textContent += dialog[index].text.charAt(letter);
          typeCharacter(index, letter + 1, speed)
        }, speed);
      } else {
        if (completeFn != null) {
          completeFn.call();
        }
      }
    }
  }
}
.as-console-wrapper .as-console-row-code,
.as-console-wrapper .as-console-row::after,
#lorem p {
  font-size: smaller;
}
<div id="lorem"></div>


Here is similar code, but less-coupled.

var dialog = [{
  text : "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
  speed : 120
}, {
  text : "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
  speed : 60
}, {
  text : "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
  speed : 90
}, {
  text : "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
  speed : 30
}];

let estimatedTime = dialog.reduce((d, e) => d + ((1000 / e.speed) * e.text.length) / 1000, 0);
console.log(`Estimated time: ${estimatedTime.toFixed(3)} seconds`);

let startTime = new Date().getTime();
typeDialog(dialog, {
  targetEl : document.getElementById('lorem'),
  completeFn : () => {
    let endTime = new Date().getTime();
    let duration = (endTime - startTime) / 1000;
    let latency = duration - estimatedTime;
    console.log(`Task took: ${duration.toFixed(3)} seconds`);
    console.log(`Latency: ${latency.toFixed(3)} seconds`);
  }
});

// Wrapper - sets up options
function typeDialog(dialog, options) {
  options = Object.assign({
    list : dialog, row : 0, col : 0,
    currentParagraph : document.createElement('p'),
    speed : 1000 / dialog[0].speed,
    targetEl : options.targetEl || ((el) => {
      document.body.appendChild(el);
      return el;
    })(document.createElement('DIV'))
  }, options || {});
  options.targetEl.appendChild(options.currentParagraph);
  repeater(typeDialogInner, options, true);
}

// Control - updated options and performs writing
function typeDialogInner(options) {
  if (options.col > options.list[options.row].text.length) {
    options.row++; options.col = 0;
    if (options.row < options.list.length) {
      options.currentParagraph = document.createElement('p');
      options.speed = 1000 / options.list[options.row].speed;
      options.targetEl.appendChild(options.currentParagraph);
    }
  }
  if (options.row < options.list.length) {
    options.currentParagraph.textContent += options.list[options.row].text.charAt(options.col++);
    return true;
  }
  return false;
}

// Utility - convenience function to handle the looping
function repeater(fn, options, repeat) {
  if (repeat) {
    setTimeout(() => repeater(fn, options, fn(options)), options.speed);
  } else {
    if (options.completeFn != null) {
      options.completeFn.call();
    }
  }
}
.as-console-wrapper .as-console-row-code,
.as-console-wrapper .as-console-row::after,
#lorem p {
  font-size: smaller;
}
<div id="lorem"></div>

Related