paint-brush
An Elegant Three Button HTML and JavaScript Audio Animation Playerby@bobnoxious
175 reads

An Elegant Three Button HTML and JavaScript Audio Animation Player

by Bob WrightFebruary 28th, 2023
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

This article describes the HTML and JavaScript code used to build an **accessible responsive design** for the Web. All of the code and content is available on a **GitHub repository** and there is a **[demo webpage]** as well.
featured image - An Elegant Three Button HTML and JavaScript Audio Animation Player
Bob Wright HackerNoon profile picture

This article describes the HTML and JavaScript code used to build an accessible responsive design animation and audio player for the Web. All of the code and content are available on a GitHub repository, and there is a demo webpage as well.


The demo code leverages some modern design aspects of HTML5 elements, CSS3, and JavaScript.

Demo HTML

The demo is a sequential content document where one “page” or panel in the document is followed by another, each in its turn. The document could be a manual, illustrated novel, or comic book as content examples.


The document may be navigated with the TAB key as well as by the mouse, and the current panel will then be highlighted.


The entire HTML code for the demo site only runs to 460 lines, not a particularly lengthy piece, and you may benefit from looking at the whole listing on the repo. For our description here, we will only look at some excerpted sections of the code along with the associated web page displays.


Here we have the first-panel display from our demo. Note that it has scaled so that it fills the screen while keeping its aspect ratio.


The demo first-page display

Immediately below the image panel, we have a button bar and a text panel that tells us a bit about the page. The text panel would ordinarily contain the document text instead of this descriptive text about the content.

The demo first-page caption and play button

Finally, if we click on the button bar or the image itself, we are presented with this animation on the page as shown here next.

The demo's first-panel animation

The HTML code behind these page displays above is shown here below. You can see that we have leveraged the features of the <picture> element in the page code.


The browser examines each one of the proffered image listings and selects the first image in the list of <source> elements that it deems suitable.


You may also see the code that presents the Play and Reload buttons SVG images and the code that displays the alternate text panel and the alternate image.


It is important to note that the browser does not download any of the listed images to determine which to use; instead, it makes its choice based on the <source> items list values and downloads only that image.


<!-- ++++++++++++++++++++ -->
<span class="clickMeOverlay" id="c1" title="Click to show alternate content">
<picture class="playGIF src" id="pic1">
	<source srcset = "./AVPlayer/01_trilobe_flux_propulsion_unit-s-0921.avif" media = "(max-width: 576px)" type = "image/avif">
	<source srcset = "./AVPlayer/01_trilobe_flux_propulsion_unit-m-1228.avif" media = "((min-width: 577px) and (max-width: 768px))" type = "image/avif">
	<source srcset = "./AVPlayer/01_trilobe_flux_propulsion_unit-l-1587.avif" media = "((min-width: 769px) and (max-width: 992px))" type = "image/avif">
	<source srcset = "./AVPlayer/01_trilobe_flux_propulsion_unit-x-1920.avif" media = "((min-width: 993px) and (max-width: 1200px))" type = "image/avif">
	<source srcset = "./AVPlayer/01_trilobe_flux_propulsion_unit-X-2240.avif" media = "(min-width: 1201px)" type = "image/avif">
	<source srcset = "./AVPlayer/01_trilobe_flux_propulsion_unit-s-0921.webp" media = "(max-width: 576px)" type = "image/webp">
	<source srcset = "./AVPlayer/01_trilobe_flux_propulsion_unit-m-1228.webp" media = "((min-width: 577px) and (max-width: 768px))" type = "image/webp">
	<source srcset = "./AVPlayer/01_trilobe_flux_propulsion_unit-l-1587.webp" media = "((min-width: 769px) and (max-width: 992px))" type = "image/webp">
	<source srcset = "./AVPlayer/01_trilobe_flux_propulsion_unit-x-1920.webp" media = "((min-width: 993px) and (max-width: 1200px))" type = "image/webp">
	<source srcset = "./AVPlayer/01_trilobe_flux_propulsion_unit-X-2240.webp" media = "(min-width: 1201px)" type = "image/webp">
	<source srcset = "./AVPlayer/01_trilobe_flux_propulsion_unit-s-0921.jpg" media = "(max-width: 576px)" type = "image/jpg">
	<source srcset = "./AVPlayer/01_trilobe_flux_propulsion_unit-m-1228.jpg" media = "((min-width: 577px) and (max-width: 768px))" type = "image/jpg">
	<source srcset = "./AVPlayer/01_trilobe_flux_propulsion_unit-l-1587.jpg" media = "((min-width: 769px) and (max-width: 992px))" type = "image/jpg">
	<source srcset = "./AVPlayer/01_trilobe_flux_propulsion_unit-x-1920.jpg" media = "((min-width: 993px) and (max-width: 1200px))" type = "image/jpg">
	<source srcset = "./AVPlayer/01_trilobe_flux_propulsion_unit-X-2240.jpg" media = "(min-width: 1201px)" type = "image/jpg">
	<img tabindex="0" id="s1" class="playGIF src" src = "./AVPlayer/01_trilobe_flux_propulsion_unit-X-2240.jpg" alt = "This image is 01_trilobe_flux_propulsion_unit." style="display: block;">
</picture>
<picture class="playGIF alt" id="apic1">
	<source srcset = "./AVPlayer/altImgs/01_trilobe_flux_propulsion_unit-w593-h374.avif" media = "(min-width: 320px)" type = "image/avif">
	<source srcset = "./AVPlayer/altImgs/01_trilobe_flux_propulsion_unit-w593-h374.webp" media = "(min-width: 320px)" type = "image/webp">
	<source srcset = "./AVPlayer/altImgs/01_trilobe_flux_propulsion_unit-w593-h374.gif" media = "(min-width: 320px)" media = "(min-width: 320px)" type = "image/gif">
	<img tabindex="0" id="a1" class="playGIF alt" src = "./AVPlayer/altImgs/01_trilobe_flux_propulsion_unit-w593-h374.gif" alt = "The shuttlecraft lands on the asteroid." style="display: none;">
</picture>
</span>
<div class="card col-12 px-sm-0" style="opacity: 0;"><br></div>
<div class="row">
	<div id="b1" tabindex="0" title="show alternate content" class="playButton clickMeOverlay card col-12 d-flex flex-column px-sm-0 align-items-center">
	<svg id="p1" title="Play Button" width="8vw" height="8vw" viewBox="0 0 16 16" class="bi bi-play" fill="white" stroke="blue" stroke-width=".5" xmlns="http://www.w3.org/2000/svg">
	<path fill-rule="evenodd" d="M10.804 8L5 4.633v6.734L10.804 8zm.792-.696a.802.802 0 0 1 0 1.392l-6.363 3.692C4.713 12.69 4 12.345 4 11.692V4.308c0-.653.713-.998 1.233-.696l6.363 3.692z"/>
	</svg>
	<svg id="r1" title="Reload Button" width="7vw" height="7vw"  viewBox="0 -1 16 16" class="bi bi-arrow-counterclockwise" fill="white" stroke="blue" stroke-width=".5" xmlns="http://www.w3.org/2000/svg">
	<path fill-rule="evenodd" d="M12.83 6.706a5 5 0 0 0-7.103-3.16.5.5 0 1 1-.454-.892A6 6 0 1 1 2.545 5.5a.5.5 0 1 1 .91.417 5 5 0 1 0 9.375.789z"/>
	<path fill-rule="evenodd" d="M7.854.146a.5.5 0 0 0-.708 0l-2.5 2.5a.5.5 0 0 0 0 .708l2.5 2.5a.5.5 0 1 0 .708-.708L5.707 3 7.854.854a.5.5 0 0 0 0-.708z"/>
	</svg>
</div></div>
<div class="card col-12 px-sm-0" style="opacity: 0;"><br></div>
<div class="xmplc card col-12 d-flex shadow-md px-sm-0">
<div tabindex="0" class="card-body" id="caption1" style="display: block;"><p>This panel shows the trilobe quantum flux concentrator propulsion unit.<br><br> For our demo, this panel uses and displays just one of the AVPlayer buttons, and that PLAY button simply launches an animation with no audio. The alternate content can also be displayed by clicking the image itself.</p>
</div>
<div tabindex="0" class="card-body xmpla" id="altcap1" style="display: none;"><p>This panel shows an animation of a shuttlecraft using the trilobe quantum flux concentrator propulsion unit.<br><br>On this panel, the RELOAD button just unloads the animation and returns to the original page. The original image can also be reloaded by clicking this image.</p>
</div>
</div>
<div class="card col-12 px-sm-0" style="opacity: 0;"><br><br></div>
<!-- ++++++++++++++++++++ -->


So indeed, which one does the browser choose? If the page URL used to launch the demo site has a query string of ?info appended it will cause an addition to our page displays which is an information annotation to each image.


This example shows the results from the two image pages we saw above.

The first-panel info addition

The animation panel info display addition

Of course, we could also look at the Browser Debugger screen which, in this case, is as shown here for the initial panel display.

Debug info for first-panel image display

And like this for the second, animated panel.

Debug info for the animation panel

We can see that the initial coded values we had for the <img> element within each <picture> element have been modified by the browser to reflect its choice from each <picture> element’s <source> list.


Here on my Chrome desktop browser, it chose to use images of AVIF file type for both the initial still image panel and for the animation panel as well.


In the case of the first panel still image, it had the choice from among three image filetypes: AVIF, WEBP, and JPG, and further, there are 5 dimension sizes for each filetype.


For our demo animation panels, we only have one size of the three file types: AVIF, WEBP, and GIF. As it turns out, as of today (2/24/23), the Edge browser does not support AVIF images, but by using the <picture> element and <source> elements, it will simply ignore those list entries.


I like that behavior. Less well-behaved are the iPhone and Firefox. The iPhone will elect to use an AVIF file from the <source> list but not display it, only the alt text. Firefox says it supports AVIF images and it does for still images. It does not show AVIF animations animated.


Somewhere, I read that one should use “feature detection”. I extended my definition of “feature” to include the platform or the browser, so I determined to use a simple redirect to send iPhone and Firefox users to an alternate page URL sans AVIF content.


To accomplish the dirty deed, I placed this JavaScript in the <head> section of my HTML.

// safari mobile does not display AVIF animation files
window.addEventListener("load", () => {
    //console.info("index "+navigator.userAgent.indexOf("Edg"));
    console.info("user agent "+navigator.userAgent);
  if ((navigator.userAgent.indexOf("iPhone") != -1 ) || (navigator.userAgent.indexOf("Firefox") != -1 )) {
	  console.info("its an iPhone or Firefox");
              window.location.replace("https://syntheticreality.net/Comics/AVPlayers.html");
  } else {
    console.info("not an iPhone or Firefox");
  }
  });
</script>

Before we look at the main JavaScript reader.js program, let’s look at the second panel in our demo, one which uses all three of the Player buttons. This is that image panel, which is again scaled to fit the screen.

Demo panel two

And below this panel, we have the Player buttons and a text panel.

Panel two Player buttons and text panel

By default, the Player is muted; that is audio is disabled as shown here. If the red “audio is muted” icon appears, it also means that there is in fact audio. To hear it, we need to click the audio icon to enable sound. The volume will be your system volume setting; keeps it simple.


The second button labeled CC lets the user disable or enable the display of a closed caption text transcript for the audio content.

Audio and closed captions enabled

Now, clicking the Play button or the image itself will launch an animation with audio, and an alternate Reload button will replace the Play button as seen here next. This example also includes the Transcript text display that shows as a result of CC being enabled.

Second-panel animation info

This is the HTML code used to render this panel and its animation, and we may readily note its resemblance to the code for the first panel.

<span class="clickMeOverlay" id="c2" title="Click to show alternate content">
<picture class="playGIF playMP3 src" id="pic2">
	<source srcset = "./AVPlayer/02_oh_no-s-0921.avif" media = "(max-width: 576px)" type = "image/avif">
	<source srcset = "./AVPlayer/02_oh_no-m-1228.avif" media = "((min-width: 577px) and (max-width: 768px))" type = "image/avif">
	<source srcset = "./AVPlayer/02_oh_no-l-1587.avif" media = "((min-width: 769px) and (max-width: 992px))" type = "image/avif">
	<source srcset = "./AVPlayer/02_oh_no-x-1920.avif" media = "((min-width: 993px) and (max-width: 1200px))" type = "image/avif">
	<source srcset = "./AVPlayer/02_oh_no-X-2240.avif" media = "(min-width: 1201px)" type = "image/avif">
	<source srcset = "./AVPlayer/02_oh_no-s-0921.webp" media = "(max-width: 576px)" type = "image/webp">
	<source srcset = "./AVPlayer/02_oh_no-m-1228.webp" media = "((min-width: 577px) and (max-width: 768px))" type = "image/webp">
	<source srcset = "./AVPlayer/02_oh_no-l-1587.webp" media = "((min-width: 769px) and (max-width: 992px))" type = "image/webp">
	<source srcset = "./AVPlayer/02_oh_no-x-1920.webp" media = "((min-width: 993px) and (max-width: 1200px))" type = "image/webp">
	<source srcset = "./AVPlayer/02_oh_no-X-2240.webp" media = "(min-width: 1201px)" type = "image/webp">
	<source srcset = "./AVPlayer/02_oh_no-s-0921.jpg" media = "(max-width: 576px)" type = "image/jpg">
	<source srcset = "./AVPlayer/02_oh_no-m-1228.jpg" media = "((min-width: 577px) and (max-width: 768px))" type = "image/jpg">
	<source srcset = "./AVPlayer/02_oh_no-l-1587.jpg" media = "((min-width: 769px) and (max-width: 992px))" type = "image/jpg">
	<source srcset = "./AVPlayer/02_oh_no-x-1920.jpg" media = "((min-width: 993px) and (max-width: 1200px))" type = "image/jpg">
	<source srcset = "./AVPlayer/02_oh_no-X-2240.jpg" media = "(min-width: 1201px)" type = "image/jpg">
	<img tabindex="0" id="s2" class="playGIF src" src = "./AVPlayer/02_oh_no-X-2240.jpg" alt = "This image is 02_oh_no." style="display: block;">
</picture>
<picture class="playGIF playMP3 alt" id="apic2">
	<source srcset = "./AVPlayer/altImgs/02_oh_no-w513-h720.avif" media = "(min-width: 320px)" type = "image/avif">
	<source srcset = "./AVPlayer/altImgs/02_oh_no-w513-h720.webp" media = "(min-width: 320px)" type = "image/webp">
	<source srcset = "./AVPlayer/altImgs/02_oh_no-w513-h720.gif" media = "(min-width: 320px)" media = "(min-width: 320px)" type = "image/gif">
	<img tabindex="0" id="a2" class="playGIF alt" src = "./AVPlayer/altImgs/02_oh_no-w513-h720.gif" alt = "The shuttlecraft lands on the asteroid." style="display: none;">
</picture>
</span>
<div class="card col-12 px-sm-0" style="opacity: 0;"><br></div>
	<div class="row">
	<div tabindex="0" title="toggle audio mute" class="mute-audio card col-2 d-flex flex-column px-sm-0 MP3Overlay align-items-center">
	<svg title="mute-audio" width="8vw" height="8vw" viewBox="0 0 16 16" class="bi bi-volume-mute" fill="white" stroke="red" stroke-width=".5" xmlns="http://www.w3.org/2000/svg">
	  <path fill-rule="evenodd" d="M6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06zM6 5.04L4.312 6.39A.5.5 0 0 1 4 6.5H2v3h2a.5.5 0 0 1 .312.11L6 10.96V5.04zm7.854.606a.5.5 0 0 1 0 .708l-4 4a.5.5 0 0 1-.708-.708l4-4a.5.5 0 0 1 .708 0z"/>
	  <path fill-rule="evenodd" d="M9.146 5.646a.5.5 0 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708l-4-4a.5.5 0 0 0-.708 0z"/>
	</svg>
	<svg title="enable-audio" width="8vw" height="8vw" viewBox="0 0 16 16" class="bi bi-volume-up" fill="white" stroke="green" stroke-width=".5" xmlns="http://www.w3.org/2000/svg">
	  <path fill-rule="evenodd" d="M6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06zM6 5.04L4.312 6.39A.5.5 0 0 1 4 6.5H2v3h2a.5.5 0 0 1 .312.11L6 10.96V5.04z"/>
	  <path d="M11.536 14.01A8.473 8.473 0 0 0 14.026 8a8.473 8.473 0 0 0-2.49-6.01l-.708.707A7.476 7.476 0 0 1 13.025 8c0 2.071-.84 3.946-2.197 5.303l.708.707z"/>
	  <path d="M10.121 12.596A6.48 6.48 0 0 0 12.025 8a6.48 6.48 0 0 0-1.904-4.596l-.707.707A5.483 5.483 0 0 1 11.025 8a5.483 5.483 0 0 1-1.61 3.89l.706.706z"/>
	  <path d="M8.707 11.182A4.486 4.486 0 0 0 10.025 8a4.486 4.486 0 0 0-1.318-3.182L8 5.525A3.489 3.489 0 0 1 9.025 8 3.49 3.49 0 0 1 8 10.475l.707.707z"/>
	</svg></div>
	<div tabindex="0" title="toggle transcript display" class="card col-2 d-flex flex-column px-sm-0 transcriptControl align-items-center">
	<svg style="padding-top: 1vw;" title="no-cc" xmlns="http://www.w3.org/2000/svg" width="7vw" height="7vw" stroke="red" stroke-width=".5" fill="white" class="bi bi-x-box" viewBox="0 0 16 16">
	<path d="M5.18 4.616a.5.5 0 0 1 .704.064L8 7.219l2.116-2.54a.5.5 0 1 1 .768.641L8.651 8l2.233 2.68a.5.5 0 0 1-.768.64L8 8.781l-2.116 2.54a.5.5 0 0 1-.768-.641L7.349 8 5.116 5.32a.5.5 0 0 1 .064-.704z"/>
	<path d="M4 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H4zm0 1h8a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1z"/>
	</svg>
	<svg title="show-cc" xmlns="http://www.w3.org/2000/svg" width="8vw" height="8vw"  stroke="blue" stroke-width=".5" fill="white" class="bi bi-badge-cc" viewBox="0 0 16 16">
	  <path d="M3.708 7.755c0-1.111.488-1.753 1.319-1.753.681 0 1.138.47 1.186 1.107H7.36V7c-.052-1.186-1.024-2-2.342-2C3.414 5 2.5 6.05 2.5 7.751v.747c0 1.7.905 2.73 2.518 2.73 1.314 0 2.285-.792 2.342-1.939v-.114H6.213c-.048.615-.496 1.05-1.186 1.05-.84 0-1.319-.62-1.319-1.727v-.743zm6.14 0c0-1.111.488-1.753 1.318-1.753.682 0 1.139.47 1.187 1.107H13.5V7c-.053-1.186-1.024-2-2.342-2C9.554 5 8.64 6.05 8.64 7.751v.747c0 1.7.905 2.73 2.518 2.73 1.314 0 2.285-.792 2.342-1.939v-.114h-1.147c-.048.615-.497 1.05-1.187 1.05-.839 0-1.318-.62-1.318-1.727v-.743z"/>
	  <path d="M14 3a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h12zM2 2a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H2z"/>
	</svg></div>

	<div id="b2" tabindex="0" title="show alternate content" class="playButton clickMeOverlay card col-8 d-flex flex-column px-sm-0 align-items-center">
	<svg id="p2" title="Play Button" width="8vw" height="8vw" viewBox="0 0 16 16" class="bi bi-play" fill="white" stroke="blue" stroke-width=".5" xmlns="http://www.w3.org/2000/svg">
	<path fill-rule="evenodd" d="M10.804 8L5 4.633v6.734L10.804 8zm.792-.696a.802.802 0 0 1 0 1.392l-6.363 3.692C4.713 12.69 4 12.345 4 11.692V4.308c0-.653.713-.998 1.233-.696l6.363 3.692z"/>
	</svg>
	<svg id="r2" title="Reload Button" width="7vw" height="7vw"  viewBox="0 -1 16 16" class="bi bi-arrow-counterclockwise" fill="white" stroke="blue" stroke-width=".5" xmlns="http://www.w3.org/2000/svg">
	<path fill-rule="evenodd" d="M12.83 6.706a5 5 0 0 0-7.103-3.16.5.5 0 1 1-.454-.892A6 6 0 1 1 2.545 5.5a.5.5 0 1 1 .91.417 5 5 0 1 0 9.375.789z"/>
	<path fill-rule="evenodd" d="M7.854.146a.5.5 0 0 0-.708 0l-2.5 2.5a.5.5 0 0 0 0 .708l2.5 2.5a.5.5 0 1 0 .708-.708L5.707 3 7.854.854a.5.5 0 0 0 0-.708z"/>
	</svg></div>
	</div>
<div class="card col-12 px-sm-0" style="opacity: 0;"><br></div>
<div id="t2" class="col-12 px-sm-0 transcript card"><p>Audio Transcript: Telephone Intercept Tones and woman saying 'oh no'.</p></div>
<audio id="audio2" src="./AVPlayer/altImgs/02_oh_no.mp3" type="audio/mpeg" alt="Telephone Intercept Tones and woman saying 'oh no'">No Audio Support</audio>
<div class="xmplc card col-12 d-flex shadow-md px-sm-0"><div tabindex="0" class="card-body" id="caption2" style="display: block;"><p>This panel uses all three of the AVPlayer buttons.<br><br>The first button controls audio muting and the default is muted audio. This button enables or disables the audio playback for all of the panels.<br><br>The second button aids in the important area of content accessibility. It enables or disables the display of a &#34;closed caption&#34; or text transcript description of the audio content for all of the panels.<br><br>The third button is the PLAY button and it plays the audio and animation content. The alternate content can also be displayed by clicking the image itself.</p></div><div tabindex="0" class="card-body xmpla" id="altcap2" style="display: none;"><p>This panel also uses all three of the AVPlayer buttons.<br><br>But now the third button is the RELOAD button and it reloads the audio and animation content. The original image can also be reloaded by clicking this image.</p></div></div>
	<div class="card col-12 px-sm-0" style="opacity: 0;"><br><br></div>
	<!-- ++++++++++++++++++++ -->


The <picture> sections are much the same for both the first and second panel HTML. The second panel HTML adds code for the SVG Audio and CC buttons to the Play/Reload button we saw before.


Also added is a small bit to display the Transcript text and to play the audio if it is not muted.


There are three more panels in the Demo; let’s briefly describe each of them. The third panel is actually a still image replacing another still image, but both images are the same, so it just plays an audio file with no change in appearance.


The fourth panel in the Demo shows an animation already running, and the Player is used to load an alternate animation. The fifth and final panel does not have an initial image to be replaced; usually the Play button inserts and plays an animation directly above it.


You could instead insert the animation BELOW the Play button as is done in the Demo.


Now, the browser and the HTML <picture> element do a fairly good job of selecting the image to download. We have a good range of granularity in our choice of image dimensions, so there is a fair chance that one will be close.


Once the browser has done the heavy lifting of choosing and downloading the best image, we could use some CSS to nicely fit it to our screen. However, we want to do more than squeeze or stretch to fit with our content, so we use a JavaScript program.

Demo JavaScript Code

The Demo JavaScript program, reader.js, is included in the GitHub repo. Our JavaScript program does two things. In the HTML code that invokes the JavaScript, we have the parameter defer which causes the program to delay its execution.


First, the script determines which image the browser has selected and downloaded from the <source> list, and then it scales that image to the viewport dimensions for the best fit while maintaining the image aspect ratio.


It looks for a window resize event and adjusts the scaling accordingly. And it gathers some information about the image. Here below, is the first part of the script.

// Reader.js
// scale the images to fill the viewport and keep aspect
// this is for demo and has extra tagging/labeling features
// comment out the console.info messages once you figure it out
 var showDetails = 0;
 var showVPDetails = 0;
 var clicked = 2;
 var currentImgID = 0;
 var viewportWidth = $(window).width();
 var viewportHeight = $(window).height();
 var elWidth = 0;
 var elHeight = 0;
 var scale = 1;
 var mp3Count = 0;
 var AltDataMsg = "";
 //var audio;
 var currentImgFolder = "";
 var currentBase = "";
 var currentMP3 = "";
	
 // browser needs to decide which source image to load for each image element
 // before it can tell us which it is, use window onload instead of jquery ready

window.onload = function() {
let searchParams = new URLSearchParams(window.location.search);
if(searchParams.has('info')) { // show some details about the images on query
 var showDetails = 1;
	console.info("show details true");
} // true
$(window).resize(function() {
	if(showVPDetails == 1) {
		$("body").append('<div id="viewport-size" style="display:block;color:#fff;background:#08F;position:fixed;top:0;left:0;font-size:2vw;z-index:5;"></div>');}
	var viewportWidth = $(window).width();
	var viewportHeight = $(window).height();
	var VPaspectRatio = viewportWidth / viewportHeight;
	var VPaspectRounded = (Math.round(VPaspectRatio * 100)) / 100;
	  // console.info("rounded VP aspect " + VPaspectRounded);
 	if(showVPDetails == 1) {
	$("#viewport-size").html('<div class="dimensions">' + viewportWidth + ' &times; ' + viewportHeight + ' px &amp; w/h = ' + VPaspectRounded + ' </div>');}

	// delete old info cards on resize
	$(".info").each(function() {
	  this.remove();
	});
	// get total src image count not including alt images
	var pnlmatched = $(".imgblock img.src");
	var pnlimgCount = pnlmatched.length;
	console.info("Number of src panels = " + pnlimgCount);
	// get total alt image count
	var altmatched = $(".imgblock img.alt");
	var altimgCount = altmatched.length;
	console.info("Number of alt panels = " + altimgCount);
	// get total alt audio count
	var mp3matched = $(".imgblock .playMP3");
	var mp3Count = mp3matched.length / 2;
	console.info("Number of alt panels with audio = " + mp3Count);

	var matched = $(".imgblock img");
	var imgCount = matched.length;
	console.info("Total Number of images/panels = " + imgCount);

	// loop through each image and tag it with an "id"
	matched.each(function() {
		console.info("================");
		console.info("currentSource "+ this.currentSrc);
		currentImgID = (this.getAttribute("id"));
			console.info("current img ID = "+ currentImgID);
		//});
		//console.info("next index "+ imgcounter);

	if (this.currentSrc.endsWith(".gif")) {
	  // get the image dimensions, faster to have sizes already specified
		if (this.currentSrc.includes("-w") && this.currentSrc.includes("-h")) {
	    var fnameLen = this.currentSrc.indexOf(".gif");
	    var elWidth = this.currentSrc.substr((this.currentSrc.indexOf("-w") + 2), 3 );
	    var elHeight = this.currentSrc.substr((this.currentSrc.indexOf("-h") + 2), 3 );
		}
	}
	if (this.currentSrc.endsWith(".avif")) {
	  // get the image dimensions, faster to have sizes already specified
		if (this.currentSrc.includes("-w") && this.currentSrc.includes("-h")) {
	    var fnameLen = this.currentSrc.indexOf(".avif");
	    var elWidth = this.currentSrc.substr((this.currentSrc.indexOf("-w") + 2), 3 );
	    var elHeight = this.currentSrc.substr((this.currentSrc.indexOf("-h") + 2), 3 );
		}
	}
	if (this.currentSrc.endsWith(".webp")) {
	  // get the image dimensions, faster to have sizes already specified
		if (this.currentSrc.includes("-w") && this.currentSrc.includes("-h")) {
	    var fnameLen = this.currentSrc.indexOf(".webp");
	    var elWidth = this.currentSrc.substr((this.currentSrc.indexOf("-w") + 2), 3 );
	    var elHeight = this.currentSrc.substr((this.currentSrc.indexOf("-h") + 2), 3 );
		}
	}
/*if (document.body.classList.includes("no-avif")) {	avifSpt = 'no-avif';
	console.info('avifSpt ' + avifSpt);}
	if (this.currentSrc.endsWith(".avif") && avifSpt == 'no-avif') {
	nameLength = (this.currentSrc.length ) - 4;
	nameString = this.currentSrc.substr(0, nameLength) + 'webp';
	console.info('nameString ' + nameString);
	this.currentSrc = nameString;
	}
*/

	if ((this.currentSrc.endsWith(".webp")) || (this.currentSrc.endsWith(".jpg")) || this.currentSrc.endsWith(".avif")) {

		if (this.currentSrc.includes("-s-")) {
	  // get the image dimensions, faster to have sizes already specified
	    var elWidth = 576;
	    var elHeight = this.currentSrc.substr((this.currentSrc.indexOf("-s-") + 3 ), 4 );
		}
		if (this.currentSrc.includes("-m-")) {
	  // get the image dimensions, faster to have sizes already specified
	    var elWidth = 768;
	    var elHeight = this.currentSrc.substr((this.currentSrc.indexOf("-m-") + 3 ), 4 );
		}
		if (this.currentSrc.includes("-l-")) {
	  // get the image dimensions, faster to have sizes already specified
	    var elWidth = 992;
	    var elHeight = this.currentSrc.substr((this.currentSrc.indexOf("-l-") + 3 ), 4 );
		}
		if (this.currentSrc.includes("-x-")) {
	  // get the image dimensions, faster to have sizes already specified
	    var elWidth = 1200;
	    var elHeight = this.currentSrc.substr((this.currentSrc.indexOf("-x-") + 3 ), 4 );
		}
		if (this.currentSrc.includes("-X-")) {
	  // get the image dimensions, faster to have sizes already specified
	    var elWidth = 1400;
	    var elHeight = this.currentSrc.substr((this.currentSrc.indexOf("-X-") + 3 ), 4 );
		}
	}
		console.info("nW "+elWidth);
		console.info("nH "+ elHeight);
		 elWidth = parseInt(elWidth);
		 elHeight = parseInt(elHeight);
		 viewportWidth = parseInt(viewportWidth);
		 viewportHeight = parseInt(viewportHeight);
		  //console.info("eW "+elWidth);
		  //console.info("eH "+elHeight);
		 // console.info("vW "+viewportWidth);
		 // console.info("vH "+viewportHeight);
		var aspect = elWidth/elHeight;
		var aspectRounded = (Math.round(aspect * 100)) / 100;
		 // console.info("rounded img aspect " + aspectRounded);
		var widthRatio = viewportWidth / elWidth;
		var heightRatio = viewportHeight / elHeight;
		 // console.info("wR "+widthRatio);
		 // console.info("hR "+heightRatio);
		 // default to the width ratio until proven wrong
		var scale = widthRatio;
		if (widthRatio * elHeight > viewportHeight) {
			scale = heightRatio;};
		var scaleRounded = (Math.round(scale * 100)) / 100;
		//  console.info("rounded scale " + scaleRounded);
		//  fit the content into the window
	// checkpoint for 1x1 image
	if ((elHeight == 1) && (elHeight == 1)) {
		hsize = elWidth;
		vsize = elHeight;
	} else {
		var hsize  = Math.round(elWidth * scale);
		var vsize = Math.round(elHeight * scale);
	}
		 console.info ("hsize "+hsize);
		 console.info ("vsize "+vsize);
	  // finally set the scaled image width and height attributes
		this.setAttribute("width", hsize);
		this.setAttribute("height", vsize);
		this.setAttribute("src", this.currentSrc);
	
	  // for the demo show a bunch of info about the image as displayed
		// parse out the source name and folder for messages and to see if we have audio
		var currentImg = this.currentSrc;
		var currentImgSource = [];
		currentImgSource = this.currentSrc.split('/');
		var currentImgFilename = currentImgSource[currentImgSource.length - 1];
		console.info("currentImgFilename "+ currentImgFilename);
		var currentImgFolder = currentImgSource[currentImgSource.length - 2];
		console.info("currentImgFolder "+ currentImgFolder);
		//currentImgPath = currentImgSource.pop();
		//console.info("currentImgPath array "+ currentImgSource);
		var currentImgName = [];
		currentImgName = currentImgFilename.split('.');
		var currentImgNoExt = currentImgName[0];
		//console.info("current Img name no extension"+ currentImgNoExt);
		var currentBasePlus = [];
		currentBasePlus = currentImgNoExt.split('-');
		var currentBase = currentBasePlus[0];
		console.info("current Img basename "+ currentBase);
		var currentMP3 = "";
		currentImg = document.getElementById(currentImgID);
		if((currentImg.classList.contains("playMP3")) && (currentImg.classList.contains("playGIF"))) {
			currentMP3 = currentBase + '.mp3';
			console.info('audio file exists = ' + currentMP3);
			AltDataMsg = "<br>There is an audio file named <span  style=\"color: darkBlue;\"><b><i>"+currentMP3+"</i></b></span> that will play if you click the panel or the play button with audio unmuted. By default the audio is muted. Click the audio icon to toggle audio muting.";
            //alert('file exists');
        } else {
			console.info('there is no audio file');
			AltDataMsg = "";
		}

		if((showDetails == 1) && (this.classList.contains("src")) && !(this.classList.contains("playGIF"))) {
			console.info("imageInfo");
			//this.setAttribute("id", imgcounter);
		// create info msg about the image
			srcid = "srcinfo"+currentImgID;
			console.info("srcinfo "+srcid);
			imageInfo = "<div id="+srcid+" class=\"info card imginfo col-12 shadow-md px-sm-0\" style=\"background-color: #b0d0ec;\"><p style=\"margin: 1vw;\">This image above is named <span style=\"color: darkBlue;\"><b><i>"+currentImgFilename+"</i></b></span> and it is panel number "+currentImgID+" of "+pnlimgCount+" total panels.<br>The source image size is "+elWidth+" X "+elHeight+" pixels for an aspect ratio of "+aspectRounded+". A scale multiplier of "+scaleRounded+" was then applied to fit the image to the viewport, resulting in the Image Display Size of "+hsize+" X "+vsize+" pixels seen here. There is no alternate image or audio for this panel.</p></div>";
		// display the info for this image
				$(this).after(imageInfo); 
				document.getElementById(srcid).style.display = "block";
		}
		if((showDetails == 1) && (currentImg.classList.contains("src")) && (currentImg.classList.contains("playGIF"))) {
		console.info("srcimginfo")
			//this.setAttribute("id", imgcounter);
		// create info msg about the image
			srcid = "srcinfo"+currentImgID;
			console.info("srcinfo "+srcid);
			srcimageInfo = "<div id="+srcid+" class=\"info card srcinfo col-12 shadow-md px-sm-0\" style=\"background-color: #b0d0ec;\"><p style=\"margin: 1vw;\">This image above is named <span style=\"color: darkBlue;\"><b><i>"+currentImgFilename+"</i></b></span> and it is panel number "+currentImgID+" of "+pnlimgCount+" total panels.<br>The source image size is "+elWidth+" X "+elHeight+" pixels for an aspect ratio of "+aspectRounded+". A scale multiplier of "+scaleRounded+" was then applied to fit the image to the viewport, resulting in the Image Display Size of "+hsize+" X "+vsize+" pixels seen here. This panel has an alternate image that will display if you click the panel or the play button."+AltDataMsg+"</p></div>";
		// display the info for this image
			$(this).after(srcimageInfo); 
			document.getElementById(srcid).style.display = "block";
		}
	
 		if ((showDetails == 1) && (currentImg.classList.contains("alt")) && (currentImg.classList.contains("playGIF"))) {
		console.info("altimginfo")
			//this.setAttribute("id", imgcounter);
			altid = "altinfo"+currentImgID;
			console.info("altinfo "+altid);
			altimageInfo = "<div id="+altid+" class=\"info card altinfo col-12 shadow-md px-sm-0\" style=\"background-color: #b0d0ec;\"><p style=\"margin: 1vw;\">This image above is named <span style=\"color: darkBlue;\"><b><i>"+currentImgFolder+'/'+currentImgFilename+"</i></b></span> and it is panel number "+currentImgID+" of "+pnlimgCount+" total panels.<br>The source image size is "+elWidth+" X "+elHeight+" pixels for an aspect ratio of "+aspectRounded+". A scale multiplier of "+scaleRounded+" was then applied to fit the image to the viewport, resulting in the Image Display Size of "+hsize+" X "+vsize+" pixels seen here. This panel is an alternate image that displays from a click on the panel or the play button."+AltDataMsg+"</p></div>";
		// display the info for this image
			$(this).after(altimageInfo); 
			document.getElementById(altid).style.display = "none";
		}
	}) // processed each matched image
}).trigger('resize'); //rescale images on viewport resize

This segment of the script shown above is executed or repeated for every image to be displayed. For a production environment, you would likely remove or disable the info gathering and display in the Demo.


This second and final section of the JavaScript program shown next is nominally “where the action is”. The first two code routines shown next are for enabling and enabling audio playback and for enabling and disabling the closed caption text transcript display.

/* ----------------------- */
// audio mute unmute toggle
$("audio").prop('muted', true); // muted by default
$(".bi-volume-up").hide(0);
$(".bi-volume-mute").show(0);
// toggle on click
  $(".mute-audio").click( function (){
	console.info("------ mute audio clicked -------");
    if( $("audio").prop('muted') ) {
          $("audio").prop('muted', false);
		  $(".bi-volume-mute").hide(0);
		  $(".bi-volume-up").show(0);
    } else {
      $("audio").prop('muted', true);
	  $(".bi-volume-up").hide(0);
	  $(".bi-volume-mute").show(0);                                 
    }
  });
// toggle on enter key
$(".mute-audio").keyup(function(event) {
  if (event.keyCode === 13) {
   event.preventDefault();
   $(".mute-audio").click();
  }
});

/* ----------------------- */
// Control transcript display
	var transtext = "active"; // active by default
	  $(".transcript").hide(0);
	  $(".bi-badge-cc").show(0);
	  $(".bi-x-box").hide(0);
// toggle display on click
$('.transcriptControl').click( function() {
	console.info('----- transcriptControl clicked -----');
	console.info("currentImgID "+ currentImgID);
	if(transtext == "notactive") {
		transtext = "active";
	  $(".bi-x-box").hide(0);
	  $(".bi-badge-cc").show(0);
	} else {
		transtext = "notactive";
	  $(".bi-badge-cc").hide(0);
	  $(".bi-x-box").show(0);
	}
	console.info("transtext "+ transtext);
});
// toggle on enter key
$(".transcriptControl").keyup(function(event) {
  if (event.keyCode === 13) {
   event.preventDefault();
   $(".transcriptControl").click();
  }
});

The final routine in our JavaScript is shown here next below. It looks for a click on elements that have the class clickMeOverlay which, in general, includes each image and its associated Play/Reload button.


Upon a click, this routine swaps the current image with its alternate, usually an animation, and on the Play side of the cycle, it will present the audio if it is not muted.

/* ----------------------- */
// control alternate img display
	  $(".bi-arrow-counterclockwise").hide(0);
	  $(".bi-play").show(0);
// toggle image to alternate img on click
// if it is a GIF it plays GIF each time clicked
$('.clickMeOverlay').click( function(event) {
	imgid = $(this).attr('id');
	imgindex = imgid.substring(1);
 	//clickaltinfo = $('.clickMeOverlay').children('.altinfo');
 	//clicksrcinfo = $('.clickMeOverlay').children('.srcinfo');
	console.info('----- play has been clicked -----');
	console.info("transtext "+ transtext);
	console.info("imgid " + imgid);
	console.info("imgindex " + imgindex);
	//console.info("mp3ID "+ mp3ID);
	source = ("s"+imgindex);
	altsource = ("a"+imgindex);
	pbutton = ("p"+imgindex);
	rbutton = ("r"+imgindex);
	tbutton = ("t"+imgindex);
	console.info("sourcetag " + source);
	console.info("altsourcetag " + altsource);
	console.info("Pbuttontag " + pbutton);
	console.info("Rbuttontag " + rbutton);
	console.info("transcripttag " + tbutton);
	aimage = document.getElementById(altsource);
	simage = document.getElementById(source);
	play = document.getElementById(pbutton);
	reload = document.getElementById(rbutton);
	tscript = document.getElementById(tbutton);

	//console.info(image);
	if(aimage.style.display == "none") {
		simage.style.display = "none";
		aimage.style.display = "block";
		play.style.display = "none";
		reload.style.display = "block";
		if(transtext == "active") {
		if(typeof(tscript) != 'undefined' && tscript != null) {
		tscript.style.display = "block";}}
		if(showDetails == 1) {
			src = document.getElementById("srcinfo"+source);
			src.style.display = "none";
			alt = document.getElementById("altinfo"+altsource);
			alt.style.display = "block";}
		cap = document.getElementById("caption" + imgindex);
		$(cap).css("display", "none");
		acap = document.getElementById("altcap" + imgindex);
		$(acap).css("display", "block");
		clicked = 1;

		console.info('----- image and caption changed -----');
		console.info("altimgID "+ imgindex);
		if ( $("audio").prop('muted') == false ) {
			console.info("audio not muted. play the audio.");
			var audio_element = document.getElementById("audio" + imgindex);
			if(typeof(audio_element) != 'undefined' && audio_element != null) {
    		audio_element.load();
    		audio_element.playclip = function(){
        		audio_element.pause();
        		audio_element.currentTime=0;
        		audio_element.play();}
			audio_element.playclip();
		}}
		} else {
		console.info('----- back to original image and caption -----');
		aimage.style.display = "none";
		simage.style.display = "block";
		reload.style.display = "none";
		play.style.display = "block";
		if(transtext == "active") {
		if(typeof(tscript) != 'undefined' && tscript != null) {
		tscript.style.display = "none";}}
		if(showDetails == 1) {
			src.style.display = "block";;
			alt.style.display = "none";}
		acap = document.getElementById("altcap" + imgindex);
		$(acap).css("display", "none");
		cap = document.getElementById("caption" + imgindex);
		$(cap).css("display", "block");
		clicked = 0;
		}
console.info("Clicked " + clicked);
});

Of final note, the reader.js program displays a good bit of info in the Console window of the browser Debugger., For production environments, you would probably trim some of that.

Conclusion

Besides serving as a tutorial example, this code will also serve as a template for a new version of my comic book builder. Stay tuned! As was mentioned previously, this Demo may be viewed on our website, and the code and content are all presented in a GitHub repository.


Hopefully, you will find some of this useful in your own web-developer endeavors. As always, your comments, criticisms, and suggestions are welcome.