this is aaronland

things I have written about elsewhere #20200914

zoomable.images.js

This was originally published on the SFO Museum Mills Field weblog, in September 2020.

We've updated the user interface elements that control how a static image can be made "zoomable" making them easier to use and more portable across a number of different settings.. We introduced "zoomable" images earlier this year. They enable a viewer to toggle between a static and a tiled version of an image allowing them to zoom in and out of details in that image.

In the first iteration the control for loading zoomable image tiles for a static image was a button located beneath the image.

In this iteration the control is graphical and sits "inline" with the image, in the top right hand corner.

When you click the "zoomable" button the tiled images are loaded fullscreen now. Before we used to load the tiles in a larger viewport than the original image but not fullscreen. We think this is a better interaction model, especially on mobile devices.

The new "zoomable" control is available on both pages with a single image as well as pages with multiple images, like search results, where a button under each image was cumbersome and distracting.

Note: From this point on I'll be covering the technical details of how these new "zoomable" controls work. If that's outside your area of interest you can stop reading here and just enjoy the improved zoomable images on the Mills Field website.

The JavaScript programming language, which is bundled with all modern graphical web browsers, is necesary in order to toggle between static and "zoomable" images. Sometimes, though, people disable JavaScript support in their web browser.

In order to ensure that "zoomable" images are an improvement to the Mills Field website, and not a requirement for using it, we've settled on a practice of starting with plain-vanilla HTML markup for images that contains structured data which can be acted on if and when JavaScript is present.

This approach owes a debt of inspiration to Shawn Allen's HTMAPL plugin for the jQuery library. HTMAPL describes itself as a "basic HTML vocabulary for describing both interactive and static geographic maps, with layers and markers". For example, you add the following HTML markup to your web pages:

<div class="map" data-center="37.765,-122.413" data-zoom="11">
  <div class="marker" data-location="37.7647463,-122.41951">
    <a href="http://stamen.com">Stamen</a>
  </div>
  <div class="controls">
    <button data-action="zoomIn">+ in</button>
    <button data-action="zoomOut">- out</button>
  </div>
</div>

And HTMAPL takes care of all the work necessary to convert that in to an interactive map. Our equivalent markup for "zoomable" images looks like this:

<div class="zoomable-image" id="zoomable-image-{IMAGE_ID}" data-image-id="{IMAGE_ID}">
  <div class="zoomable-static" id="zoomable-static-{IMAGE_ID}">

    <button class="btn btn-sm btn-light zoomable-button zoomable-toggle-tiles" id="zoomable-toggle-tiles-{IMAGE_ID}" data-id="{IMAGE_ID}" title="View this image in full screen mode"/>
    <p id="zoomable-loading-{IMAGE_ID}" class="zoomable-loading" style="background-image:url({THUMBNAIL_URL});"><span class="zoomable-loading-text">loading</span></p>

    <img id="zoomable-picture-default-{IMAGE_ID}" class="zoomable-picture-default" src="{IMAGE_URL}" alt="..." onload="zoomable.images.onload_image('{IMAGE_ID}');"/>

  </div>

  <div class="zoomable-tiles" id="zoomable-tiles-{IMAGE_ID}" data-tiles-url="{TILES_URL}">
    <div class="zoomable-map" id="zoomable-map-{IMAGE_ID}"/>
  </div>

</div>

At the top of each page we load CSS styling information for rendering the images on a page whether or not JavaScript is enabled. This includes styles for placing the "zoomable" control which is hidden by default.

<link rel="stylesheet" type="text/css" href="zoomable.images.rollup.css" />

We also load Javascript libraries which will look for all the elements in the webpage with a zoomable-image CSS class and, using structured data contained in these elements and their children make all the necessary adjustments to enable "zoomable" image controls.

<script type="text/javascript" src="zoomable.images.rollup.js"></script>
<script type="text/javascript">
window.addEventListener("load", function load(event){
    zoomable.images.init();
});
</script>

In the HTML example above there are a lot of {SOMETHING} style variables. I'll walk through the markup step-by-step in order to describe them.

<div class="zoomable-image" id="zoomable-image-{IMAGE_ID}" data-image-id="{IMAGE_ID}">

This is the top-level element for a "zoomable" image. The {IMAGE_ID} variable is whatever you want it to be. It only needs to be unique to a specific image.

  <div class="zoomable-static" id="zoomable-static-{IMAGE_ID}"">

This is the top-level element for a static image.

    
    <button class="btn btn-sm btn-light zoomable-button zoomable-toggle-tiles" id="zoomable-toggle-tiles-{IMAGE_ID}" data-id="{IMAGE_ID}" title="View this image in full screen mode"/>

This is the "zoomable" control element. It is hidden by default.

    <p id="zoomable-loading-{IMAGE_ID}" class="zoomable-loading" style="background-image:url({THUMBNAIL_URL});>
        <span class="zoomable-loading-text">loading</span>
    </p>

This is text that is overlaid on the {THUMBNAIL_URL} image, described below. Like the thumbnail image it is hidden once {IMAGE_URL}, described in the next section, is loaded.

Note the background-image CSS style, in the parent element, which contains the {THUMBNAIL_URL} variable. This is a smaller version of the static image you want to show. It will be hidden once {IMAGE_URL} is loaded.

    <img id="zoomable-picture-default-{IMAGE_ID}" class="zoomable-picture-default" src="{IMAGE_URL}" alt="..." onload="zoomable.images.onload_image('{IMAGE_ID}');"/>

This is the static image itself, defined by the {IMAGE_URL} variable. This might also be a picture element if you want to define multiple images to display based on the browser's screen size. On the Mills Field website we use picture elements for object webpages and img elements for "list view" pages, like exhibition images.

If JavaScript is enabled this element is hidden until the image is finished loading, at which point the thumbnail image and text above are hidden. If JavaScript is not enabled then the thumbnail image and text are hidden and this (image) element is visible and rendered as the image loads.

  </div>

  <div class="zoomable-tiles" id="zoomable-tiles-{IMAGE_ID}" data-tiles-url="{TILES_URL}">
    <div class="zoomable-map" id="zoomable-map-{IMAGE_ID}"/>
  </div>
  
</div>

Finally, the container element for the tiled "zoomable" image. The tiles themselves are expected to be IIIF-style image tiles and the {TILES_URL} variable defines the location of a IIIF info.json document for the image in question.

loading

timetable: WestAir Airlines
 
Image by SFO Museum.

Here's a concrete example: This is the markup we're using to display the image for a 1983 timetable for WestAir Airlines, above.

<div class="zoomable-image" id="zoomable-image-1527842879" data-image-id="1527842879">
  <div class="zoomable-static" id="zoomable-static-1527842879">
    <button class="btn btn-sm btn-light zoomable-button zoomable-toggle-tiles" id="zoomable-toggle-tiles-1527842879" data-id="1527842879" title="View this image in full screen mode"/>
    <p id="zoomable-loading-1527842879" class="zoomable-loading" style="background-image:url(https://static.sfomuseum.org/media/152/784/287/9/1527842879_Ag9JNIHX7o2LpjurG7YBMhNyV8Gqiv8h_ds.jpg)">
      <span class="zoomable-loading-text">loading</span>
    </p>
    <picture class="zoomable-picture" id="zoomable-picture-1527842879">
      <source srcset="https://static.sfomuseum.org/media/152/784/287/9/1527842879_Ag9JNIHX7o2LpjurG7YBMhNyV8Gqiv8h_c.jpg" media="(min-height:800px) and (min-width: 800px)"/>
      <source srcset="https://static.sfomuseum.org/media/152/784/287/9/1527842879_Ag9JNIHX7o2LpjurG7YBMhNyV8Gqiv8h_z.jpg" media="(min-height:400px) and (min-width: 400px)"/>
      <source srcset="https://static.sfomuseum.org/media/152/784/287/9/1527842879_Ag9JNIHX7o2LpjurG7YBMhNyV8Gqiv8h_n.jpg" media="(min-height:400px)"/>
      <source srcset="https://static.sfomuseum.org/media/152/784/287/9/1527842879_Ag9JNIHX7o2LpjurG7YBMhNyV8Gqiv8h_b.jpg" media="(min-width: 1024px)"/>
      <source srcset="https://static.sfomuseum.org/media/152/784/287/9/1527842879_Ag9JNIHX7o2LpjurG7YBMhNyV8Gqiv8h_c.jpg" media="(min-width: 800px)"/>
      <source srcset="https://static.sfomuseum.org/media/152/784/287/9/1527842879_Ag9JNIHX7o2LpjurG7YBMhNyV8Gqiv8h_z.jpg" media="(min-width: 400px)"/>
      <source srcset="https://static.sfomuseum.org/media/152/784/287/9/1527842879_Ag9JNIHX7o2LpjurG7YBMhNyV8Gqiv8h_n.jpg" media="(min-width: 320px)"/>
      <img id="zoomable-picture-default-1527842879" class="card-img-top zoomable-picture-default image-square" src="https://static.sfomuseum.org/media/152/784/287/9/Ag9JNIHX7o2LpjurG7YBMhNyV8Gqiv8h_n.jpg" alt="timetable: WestAir Airlines" onload="zoomable.images.onload_image('1527842879');"/>
    </picture>
  </div>
  <div class="zoomable-tiles" id="zoomable-tiles-1527842879" data-tiles-url="https://static.sfomuseum.org/media/152/784/287/9/tiles/">
    <div class="zoomable-map" id="zoomable-map-1527842879"/>
  </div>
  <div class="zoomable-caption" id="zoomable-caption-1527842879">
    <div class="image-attribution">Image by <a href="https://millsfield.sfomuseum.org/images/sfomuseum/">SFO Museum</a>.</div>
  </div>
</div>

The JavaScript code we use to control "zoomable" images is actually just a thin wrapper around a number of other libraries. They are the Leaflet map rendering library and the extensions necessary to use IIIF image tiles and to enable fullscreen display.

As well as our own leaflet-image-control library, and friends, which we use to enable saving images to your desktop from the detail of a zoomable image. We first wrote about this in the Map update, 2019-2020 blog post last winter.

In this way the JavaScript code that we wrote to control "zoomable" images does both quite a lot, if you count all the things that its dependecies do, and not very much at all if you only look at the functionality it alone provides. This is part of our ongoing effort to develop, and share, small and focused tools that don't try to do too much and that are amenable to being reconfigured in new and different ways depending on the desired goal.

Riven/River, Jim Melchert. 2014. Grey porcelain ceramic tile, red & blue glaze. Collection of the City and County of San Francisco Commissioned by the San Francisco Arts Commission and the Airport Commission for the San Francisco International Airport. 2016.22.a-x

For example, our "zoomable" image code doesn't know anything about producing IIIF-compatible image tiles. It knows how to consume them but how they are made is left to another software program to figure out. We've written about how we produce and use IIIF images at SFO Museum in the past and we'll write more about that in the future. We've made some substantial improvements to the workflow we use for processing images and are eager to share them.

Our hope is that if you already have IIIF image tiles that you can use them "as-is" with these particular bits of HTML markup and JavaScript code to enable simple and elegant, and pretty sophisticated, user interface controls to display "zoomable" images.

As we demonstrated above we're using it in this blog post!

This is "version 2", building and improving on the work we did to launch "zoomable" images. There are probably a few gotchas that we'll still need to address and I'd like to remove as much of the HTML markup, described above, as possible in future iterations. For example, the following bits of markup could probably be generated dynamically when a web page is loaded:

    <button class="btn btn-sm btn-light zoomable-button zoomable-toggle-tiles" id="zoomable-toggle-tiles-{IMAGE_ID}" data-id="{IMAGE_ID}" title="View this image in full screen mode"/>
    <p id="zoomable-loading-{IMAGE_ID}" class="zoomable-loading" style="background-image:url({THUMBNAIL_URL});"><span class="zoomable-loading-text">loading</span></p>

And:

  <div class="zoomable-tiles" id="zoomable-tiles-{IMAGE_ID}" data-tiles-url="{TILES_URL}">
    <div class="zoomable-map" id="zoomable-map-{IMAGE_ID}"/>
  </div>

These are elements that are only necessary if and when JavaScript is present so we may as well let JavaScript create them before it goes to work. That will be "version 3" and if we've done our job right the time to get from version 2 to version 3, from version 3 to version 4, and so on, will be faster than the time it took to get from version 1 to version 2. In many ways the most important part of this work is just practicing the art of revisiting and improving these projects in small and manageable pieces. To learn how we make these improvements at the same time we're doing everything else.

Luggage label: Varney Speed Lines. Paper, ink, adhesive. Purchase, SFO Museum Collection. 1996.35.059.

All of this code is open source and available in the js-zoomable-images repository on our GitHub account.

The repository contains both the raw code that we've written on top of the libraries I described earlier:

There are also "bundled" versions of the js-zoomable-images code which contain all of the necessary dependencies in individual JavaScript and CSS files: