Скорочтение

HTML

<!-- Inspired by http://www.spritzinc.com/ -->
<!-- Strugatskie's text is used for demo -->
<div id="help">
  Скопируйте текст в поле ввода и нажмите "ПОЕХАЛИ".
  <br/> Во время чтения доступны горячие клавиши: k — быстрее, j — медленнее, s — stop/start.
</div>
<div id="pnl-input">
  <textarea>
    Ложа Анкиного арбалета была выточена из черной пластмассы, а тетива была из хромистой стали и натягивалась одним движением бесшумно скользящего рычага. Антон новшеств не признавал: у него было доброе боевое устройство в стиле маршала Тоца, короля Пица
    Первого, окованное черной медью, с колесиком, на которое наматывался шнур из воловьих жил. Что касается Пашки, то он взял пневматический карабин. Арбалеты он считал детством человечества, так как был ленив и неспособен к столярному ремеслу. Они причалили
    к северному берегу, где из желтого песчаного обрыва торчали корявые корни мачтовых сосен. Анка бросила рулевое весло и оглянулась. Солнце уже поднялось над лесом, и все было голубое, зеленое и желтое — голубой туман над озером, темно-зеленые сосны
    и желтый берег на той стороне. И небо над всем этим было ясное, белесовато-синее. — Ничего там нет,— сказал Пашка. s Ребята сидели, перегнувшись через борт, и глядели в воду. — Громадная щука,— уверенно сказал Антон. — С вот такими плавниками? — спросил
    Пашка. Антон промолчал. Анка тоже посмотрела в воду, но увидела только собственное отражение. — Искупаться бы,— сказал Пашка, запуская руку по локоть в воду.— Холодная,— сообщил он. Антон перебрался на нос и спрыгнул на берег. Лодка закачалась. Антон
    взялся за борт и выжидательно посмотрел на Пашку. Тогда Пашка поднялся, заложил весло за шею, как коромысло, и, извиваясь нижней частью туловища, пропел: Старый шкипер Вицлипуцли! Ты, приятель, не заснул? Берегись, к тебе несутся Стаи жареных акул!
    Антон молча рванул лодку. — Эй-эй! — закричал Пашка, хватаясь за борта. — Почему жареных? — спросила Анка. — Не знаю,— ответил Пашка. Они выбрались из лодки.— А...

CSS

#pnl-read {
  width: 500px;
  height: 200px;
  margin: 50px auto;
  position: relative;
}

#frame {
  width: 150px;
  height: 20px;
  border: 2px solid red;
  border-left: none;
  border-right: none;
  position: absolute;
  left: 225px;
  top: 90px;
}

#content {
  width: 500px;
  overflow: hidden;
  position: absolute;
}

#content P {
  margin-top: 3pt;
  text-indent: 10pt;
  color: #CCC;
}

#content .cur {
  color: #000;
}

#content .cur A {
  color: #A00;
}

JavaScript

var anchors, interval = 120,
  delta = 20,
  contentElem, frameElem, timeoutId = null,
  nextIdx = 0,
  curWordSpan = null,
  usePunctuation = false;

function arr(o) {
  return Array.prototype.slice.apply(o)
}

function $(q) {
  return document.querySelector(q)
}

function $$(q) {
  return arr(document.querySelectorAll(q))
}

onload();

function onload() {
  // global
  frameElem = $('#frame');
  contentElem = $('#content');
  // local
  var pnlInput = $('#pnl-input'),
    pnlRead = $('#pnl-read'),
    btnStart = $('#pnl-input INPUT[type=button]');
  pnlInput.style.display = "block";
  pnlRead.style.display = "none";
  btnStart.addEventListener('click', function() {
    pnlInput.style.display = "none";
    pnlRead.style.display = "block";
    beginRead();
  });
  btnStart.focus();
}

function beginRead() {
  var text = $('#pnl-input TEXTAREA').value;
  text = escapeHtml(text);
  text = spreadAnchors(text);
  text = '<p>' + text.replace(/\n/g, '</p><p>') + '</p>';

  contentElem.innerHTML = text;

  anchors = $$('#content A');
  document.addEventListener('keypress', function(e) {
    switch (e.keyCode || e.which) {
      case 's'.charCodeAt(0):
        startStop();
        break;
      case 'j'.charCodeAt(0):
        slower();
        break;
      case 'k'.charCodeAt(0):
        faster();
        break;
    }
  });

  usePunctuation = $('#chk-punktuation').checked;

  tick();
}

function escapeHtml(text) {
  return text.replace(/[<>&"]/g, function(c) {
    switch (c) {
      case '<':
        return "&lt;";
      case '>':
        return "&gt;";
      case '&':
        return "&amp;";
      case '"':
        return "&quot;";
    }
  });
}

function spreadAnchors(text) {
  return text.replace(/([^\s-——,.;:!?«»"„“]+)([\s-——,.;:!?«»"„“]{0,3})/g, function(_, w, p) {
    var c = bestChar(w.length);
    return '<span>' + w.substring(0, c) +
      '<a data-l="' + w.length + '" data-p="' + punctuationCoeff(p) + '">' + w[c] + '</a>' +
      w.substring(c + 1) + '</span>'...