Focus Pulse Widget

This widget functions as an on-page reading assistant that dynamically creates a guided, active-reading experience for your blog visitors. 

When a user activates the tool, it automatically scans the main article structure while ignoring unrelated text like sidebars, navigation menus, or even its own control buttons. Once the text is isolated, the underlying code seamlessly wraps every individual word in an invisible container, prepping the entire post for a coordinated visual sequence without altering your original font styles, links, pictures, or overall layout. 

Once active, the widget acts like a digital reading pacing guide by applying a rhythmic "pulse" effect to the text. It targets one word at a time, momentarily expanding its scale and shifting its color to pull the reader's eye naturally across the screen.

As the reader progresses, the current word scales down to normal while the next word simultaneously illuminates and enlarges. Additionally, the widget includes an automated tracking feature that gently scrolls the page downward as the reader moves through the text, ensuring they never lose their place or have to manually scroll while reading.

To make the experience fully customisable, the widget features a floating control panel equipped with a responsive play/pause toggle and a speed adjustment slider.

This setup allows readers to perfectly match the visual pulsing effect to their personal reading speed, whether they want to practice rapid serial scanning or slow things down for intensive comprehension.

By transforming passive scrolling into an engaging, focused visual flow, the widget ultimately helps reduce cognitive strain, boosts retention, and assists readers with different accessibility needs or learning preferences.
Focus Pulse Widget
<style>
  .reader-word {
    display: inline-block;
    transition: transform 0.15s ease, color 0.15s ease, text-shadow 0.15s ease;
  }
  
  .reader-enlarged {
    transform: scale(1.2);
    color: #e74c3c;
    font-weight: bold;
    z-index: 10;
    position: relative;
    /* The blur effect on the pulse */
    text-shadow: 0px 4px 12px rgba(231, 76, 60, 0.6); 
  }

  #guided-reader-controls {
    position: fixed;
    bottom: 20px;
    right: 20px;
    background: #ffffff;
    padding: 12px 20px;
    border-radius: 8px;
    box-shadow: 0 4px 12px rgba(0,0,0,0.15);
    z-index: 99999;
    display: flex;
    align-items: center;
    gap: 15px;
    font-family: sans-serif;
    border: 1px solid #ddd;
  }
  
  #guided-reader-controls button {
    padding: 6px 12px;
    cursor: pointer;
    background: #0073aa;
    color: white;
    border: none;
    border-radius: 4px;
    font-weight: bold;
  }
  
  #guided-reader-controls button:hover {
    background: #005177;
  }

  /* --- Custom Slider Styling --- */
  #reader-speed {
    -webkit-appearance: none;
    appearance: none;
    width: 100%;
    height: 6px;
    background: #e0e0e0; /* The grey channel */
    border-radius: 3px;
    outline: none;
    margin-top: 8px;
  }

  /* Chrome, Safari, Edge, Opera thumb */
  #reader-speed::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: 16px;
    height: 16px;
    border-radius: 50%;
    background: #0073aa; /* The blue button */
    cursor: pointer;
    box-shadow: 0 1px 3px rgba(0,0,0,0.3);
  }

  /* Firefox thumb */
  #reader-speed::-moz-range-thumb {
    width: 16px;
    height: 16px;
    border-radius: 50%;
    background: #0073aa; /* The blue button */
    cursor: pointer;
    border: none;
    box-shadow: 0 1px 3px rgba(0,0,0,0.3);
  }
</style>

<div id="guided-reader-controls">
  <button id="reader-play-btn">Play</button>
  <div style="display: flex; flex-direction: column; font-size: 12px;">
    <label for="reader-speed">Speed (Fast -> Slow)</label>
    <input type="range" id="reader-speed" min="50" max="600" value="250">
  </div>
</div>

<script>
document.addEventListener("DOMContentLoaded", function() {
  const contentSelectors = '.entry-content, .post-body, article, .post-content';
  const contentContainer = document.querySelector(contentSelectors);

  if (!contentContainer) {
    console.log("Guided Reader: Content area not found.");
    return;
  }

  let wordsArray = [];
  
  function wrapWords(node) {
    const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, null, false);
    const textNodes = [];
    
    while(walker.nextNode()) {
      const parentElement = walker.currentNode.parentElement;
      
      if (!parentElement) continue;

      const parentTag = parentElement.tagName;
      const isInsideWidget = parentElement.closest('#guided-reader-controls');
      
      if(
        walker.currentNode.nodeValue.trim() !== '' && 
        parentTag !== 'SCRIPT' && 
        parentTag !== 'STYLE' &&
        !isInsideWidget
      ) {
        textNodes.push(walker.currentNode);
      }
    }

    textNodes.forEach(textNode => {
      const textContent = textNode.nodeValue;
      const parts = textContent.split(/(\s+)/); 
      const fragment = document.createDocumentFragment();
      
      parts.forEach(part => {
        if (part.trim().length > 0) {
          const span = document.createElement('span');
          span.className = 'reader-word';
          span.textContent = part;
          fragment.appendChild(span);
          wordsArray.push(span); 
        } else {
          fragment.appendChild(document.createTextNode(part)); 
        }
      });
      textNode.parentNode.replaceChild(fragment, textNode);
    });
  }

  wrapWords(contentContainer);

  let currentIndex = 0;
  let isPlaying = false;
  let readerInterval;
  
  const playBtn = document.getElementById('reader-play-btn');
  const speedSlider = document.getElementById('reader-speed');

  function highlightNextWord() {
    if (currentIndex > 0) {
      wordsArray[currentIndex - 1].classList.remove('reader-enlarged');
    } else if (currentIndex === 0 && wordsArray.length > 0) {
       wordsArray[wordsArray.length - 1].classList.remove('reader-enlarged');
    }

    if (currentIndex >= wordsArray.length) {
      stopReader();
      currentIndex = 0; 
      return;
    }

    wordsArray[currentIndex].classList.add('reader-enlarged');
    
    const rect = wordsArray[currentIndex].getBoundingClientRect();
    if(rect.top > window.innerHeight - 300 || rect.top < 100) {
      wordsArray[currentIndex].scrollIntoView({ behavior: 'smooth', block: 'center' });
    }

    currentIndex++;
    
    if (isPlaying) {
      readerInterval = setTimeout(highlightNextWord, parseInt(speedSlider.value));
    }
  }

  function startReader() {
    if (wordsArray.length === 0) return; 
    
    isPlaying = true;
    playBtn.textContent = "Pause";
    highlightNextWord();
  }

  function stopReader() {
    isPlaying = false;
    playBtn.textContent = "Play";
    clearTimeout(readerInterval);
  }

  playBtn.addEventListener('click', () => {
    if (isPlaying) {
      stopReader();
    } else {
      startReader();
    }
  });

  speedSlider.addEventListener('input', () => {
    if (isPlaying) {
      clearTimeout(readerInterval);
      readerInterval = setTimeout(highlightNextWord, parseInt(speedSlider.value));
    }
  });
});
</script>

Post a Comment

0 Comments

Notice:
Comments are moderated and may not appear immediately. Please keep your comments respectful, and relevant to the post. Spam will not be tolerated. My site. My rules.

Post a Comment (0)