Speed Reader

Vue.js experiment with various speed reading techniques

hack

Build

Client-side javascript application built in a weekend challenge. This page contains an overview of the stack, descriptions of some of the core functions, and a potential development roadmap. You can jump to any section you’re interested in:


Stack

Vue was the first choice for building out the components – being much lighter than Angular or React and a breeze to develop and debug. The core business logic is managed within a flux-like architecture in order to best deal with the frequency of updates and view interactions.

Using pug for templates and Postcss for styles and post processing.


Design

Token Model

Each token contains all the information needed to display and locate it within the text.

{
  text: <String>,        // contains the string value of the token (word, space, dash, ...)
  globalIndex: <Number>  // token index relative to the entire text
  modifier?: <Number>,   // multiplies with the baseline wpm value to optimize the delay for long words, clauses, etc.
  offset?: <Number>      // optimize word's center alignment based on length
  wraps?: <Object>,      // an object containing quotes or parens that should go around the `text` to indicate when it is inside a clause
  start?: <Boolean>,     // whether the token starts a new sentence (used for skipping through text)
  ignore?: <Boolean>,    // the reader should not display spaces, quotes or parentheses (at least not in the center viewport)
}



Initial State

“The single source of truth” required to create a view at any given time.

{
  active: false,    // whether text has been supplied and a new session initiated.
  playing: false,   // whether the reader is playing or paused.
  token: {},        // contains the current active token object (see model above)
  blocks: [],       // contains an array of arrays of `token` objects.
  blockIndex: 0,    // the current active paragraph.
  globalIndex: 0,   // the current active token index.
  wpm: 500          // reading speed in words per minute
}



Text Parser

Text needs to be converted from a string, such as:

Regular sentence.

(Parenthesized clause.)

to an array of arrays of tokens, like so:

[[
  {
    text: 'Regular',
    modifier: 1.79,
    offset: 2,
    globalIndex: 0,
    start: true
  }, {
    text: ' ',
    ignore: true
  }, {
    text: 'sentence.',
    modifier: 2.8,
    offset: 2,
    globalIndex: 2
  }
], [
  {
    text: '(',
    ignore: true
  }, {
    text: 'Parenthesized',
    modifier: 2,
    wraps: { LEFT: '(', RIGHT: ')' },
    offset: 1,
    globalIndex: 4,
    start: true
  }, {
    text: ' ',
    ignore: true
  }, {
    text: 'clause.',
    modifier: 1.4,
    wraps: { LEFT: '(', RIGHT: ')' },
    offset: 2,
    globalIndex: 6
  }, {
    text: ')',
    ignore: true
  }
]]

The parser function splits the text into arrays of paragraphs and then performs several filter, reduce and map operations on each paragraph to generate tokens and their various required properties. The complete function spans over 100 lines and uses several utilities so I won’t cover it here in more detail but it’s fairly straightforward and well documented if you would like to read the source. It is written in a functional style following immutability principles so it’s pretty easy to follow.



Player Function

Playing is handled by a recursive function, which receives an array from the parsed text as an argument and displays tokens based on their properties. The code is commented here for clarification and where methods have been omitted for brevity.

function play ({ commit, dispatch, state }, block) {
  if (/* beyond last block */) {
    return dispatch(/* stop playing */)
  }

  block // array of text tokens
    .reduce((promise, token) => (
      promise.then(delay => ( // delay value for the previous token
        new Promise(resolve => {
          setTimeout(() => {
            commit(/* display current token */)
            resolve(/* new delay value for current token */)
          }, delay)
        })
      ))
    ), Promise.resolve())
    .then(delay => { // delay value for the last token in paragraph
      // all promises inside the block have been resolved
      setTimeout(() => {
        dispatch(/* call self with the next block */)
      }, delay)
    })
    .catch(() => {
      // returning Promise.reject() from inside the reduce function allows breaking
      // out of the loop and stopping execution (not implemented here for brevity)
      dispatch(/* stop playing */)
    })
}

The play function receives two arguments: the store object, which is supplied by the Vuex library (and destructured here into { commit, dispatch, state }); and the block array, which contains all of the tokens in the active paragraph.

The core of this function is based on calling reduce on the array of tokens and supplying a resolved Promise as its second argument. This provides two critical features:

The second point might seem a little confusing. A call to setTimeout(fn, x) waits x seconds before executing fn(). This means that if we want to display a word for x seconds, the timeout needs to be called after the word is displayed. This is why the promise chain inside the reduce function is so critical – in each iteration, the delay is supplied by the last resolved promise.


Roadmap

You're on the Speed Reader build page