A Weird Imagination

Change title based on visible section

The problem

In the computer game Keep Talking and Nobody Explodes, the "bomb expert" players are looking at a fictional "bomb manual", often frantically searching for the right page. While the intention is for this document to be printed out—and physical paper makes it relatively easy to keep the headings at the top of each page visible—there is also a web version if you prefer to view it on a screen (or don't have access to a printer). Scrolling through the web version feels a lot more awkward than flipping through the paper version; one workaround I found was to open different pages in different browser tabs or windows, but then identifying which page is in which window is still awkward.

The solution

I created a userscript, "BombManual.com Tab Title and TOC" that automatically updates the tab title to match the title of the currently visible page. It additionally adds a table of contents to make it easy to quickly open all of the pages in separate tabs or windows.

If you do not already have one, you will need to install a userscript manager extension for your browser to use it. Alternatively, you could bookmark this bookmarklet, but as you would have to click that bookmark on every instance of the page you opened, that's less convenient.

The details

A similar common effect is highlighting the currently visible sections in a table of contents. This blog post includes that effect, which is pretty close to what we want.

The difference is for that scenario, it's okay to highlight multiple sections, but in this scenario, we want to highlight the single most pertinent section. First we need to decide what exactly we mean by that. Obviously, only the visible sections could possibly be relevant. Of those, the section with the most area visible is likely the one being looked at. If there's a tie (for example, multiple sections are fully on screen), then select the one further up on the page.

In terms of code, the Intersection Observer API is used to get notifications when an element is scrolled in or out of view. When a section is first starting to scroll into view is probably not a particularly relevant change from the user's point of view, since it could be just a single pixel on the edge of the viewport. To handle that, the API has a thresholds option to request more events. The default value is [0], which means that whenever the proportion of the element visible passes through 0 (i.e. out of view) in either direction, an event is generated. That event includes the intersectionRatio which ranges from 0 (off-screen) to 1 (fully visible). (For the website I was working on, all of the sections are exactly the same size; for other applications intersectionRect might be useful to compute the visible area.)

We can't get IntersectionObserver to inform us on every scroll, but we can approximate it by setting the thresholds option to request more events. For instance, if we want to know the proportion visible to the closest 25%, we can instead set thresholds to [0, 0.25, 0.5, 0.75, 1] (i.e. all of the multiples of 0.25). Then any time the proportion visible for one of the sections passes through a multiple of 25% by scrolling in or out of view, an event will be generated, so the estimate will never be off by more than 25%.

Furthermore, the events only contain information on the elements whose intersectionRatio have just passed through one of the thresholds, so we need to keep track of that information across events.

Putting it all together, the IntersectionObserver looks like this:

const visibleSections = new Map();
let observer = new IntersectionObserver((entries, observer) => {
  // Update new intersectionRatios.
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      visibleSections.set(entry.target, entry.intersectionRatio);
    } else {
      visibleSections.delete(entry.target);
    }
  });

  // Look through sections in order and keep the most visible section.
  let max = -1;
  let mostVisibleSection = null;
  for (const section of sections) {
    const intersectionRatio = visibleSections.get(section)
    if (intersectionRatio && intersectionRatio > max) {
      max = intersectionRatio;
      mostVisibleSection = section;
    }
  }

  // Do something with mostVisibleSection
  updateTitleFor(mostVisibleSection);
}, {
  threshold: [0, 0.25, 0.5, 0.75, 1],
});

See this JSFiddle for that code expanded into an runnable example.

Comments

Have something to add? Post a comment by sending an email to comments@aweirdimagination.net. You may use Markdown for formatting.

There are no comments yet.