JavaScript/Tutorials/Web Animations/Komfort-Bildwechsler
Bis jetzt lief die Animation immer in einer festen Reihenfolge ab. Ein komfortabler Bildwechsler soll aber auch eine zufällige Reihenfolge ermöglichen. Um unseren Animations-Player vollends zum Komfortmodell umbauen zu können, sollten wir sowohl das bisher verwendete Script als auch das HTML-Markup auf den Prüfstand stellen.
Inhaltsverzeichnis
Vorüberlegungen
Im vorletzten Kapitel ging es darum, eine Animation zu steuern,
im letzten darum, mehrere Bilder in einer Animation nacheinander einzublenden. Diese verkettete Animation hat den Nachteil, dass sie nur schwierig zu pausieren und zu steuern ist.
Ein Bildwechsler soll sowohl manuell als automatisch ablaufen können. Der Benutzer soll darüber die volle Kontrolle haben. Der automatische Wechsel soll in der vorgegebenen oder auch in zufälliger Reihenfolge ablaufen. Dabei soll beim letzten Bild auf das erste und umgekehrt gesprungen werden. Der Bildwechsler soll sowohl per Maus, Touch als auch per Tastatur bedient werden können.
Das ist aber nicht mehr mit html und css allein zu schaffen. Daher kommt jetzt auch Javascript zum Einsatz. Die Bilder sollen aber auch ohne Javascript erreichbar sein.
Dynamischer Player
Bis jetzt waren alle Bilder und die dazugehörige Bildunterschrift in einem eigenen figure-Element verpackt. Künftig sollen die Bilder einfach nacheinander notiert werden. Die Vorschaubilder (Thumbs) liegen in einem Link auf das große Bild. So können die Bilder auch ohne Javascript erreicht werden.
Ein Bildwechsler ohne Komfort findet sich bereits im Bilder im Internet-Kurs.
<section id="peru" class="gallery">
<h2>Peru 2007</h2>
<a href="peru-3.jpg">
<img src="peru-3-sm.jpg" alt="Peru 2007: Machu Picchu">
</a>
<a href="peru-4.jpg">
<img src="peru-4-sm.jpg" alt="Peru 2007: Machu Picchu - Lamas in den Ruinen">
</a>
<a href="peru-5.jpg">
<img src="peru-5-sm.jpg" alt="Peru 2007: Uros-Inseln im Titicaca-See">
</a>
...
</section>
Komfort-Bildwechsler
Player-Template
Im Unterschied zum oben erwähnten Bildwechsler, dessen Schwerpunkt auf der dynamischen Erzeugung der Bedienelemente lag, wird der Player bereits im HTML-Dokument notiert.
Der Player liegt in einem dialog-Element und wird erst per Javascript bei Klick auf ein Bild geöffnet. Das Script kopiert dann den URL des großen Bildes aus dem Link in das src-Attribut des img-Elements
Das dialog-Element enthält ein figure-Element mit img und figcaption. Die Bilder haben als thumbnail ja einen Alternativtext, der vom Script ausgelesen und in das figcaption-Element geschrieben wird. Das alt-Attribut des Großbilds weist auf diese Bildbeschreibung hin.
Daneben gibt es fünf Buttons: diese enthalten einen Textinhalt, der unsichtbar ist (visually-hidden) und nur als Hilfe für assistive Techniken gedacht ist, und ein bzw. auch zwei inline-SVGs.
Gegenüber dem Beispiel aus dem letzten Kapitel haben wir einen neuen Button: 🔀
Er soll im Shuffle-Modus die Bilder nach dem Zufallsprinzip anzeigen. Da das entsprechende Unicode-Symbol 'TWISTED RIGHTWARDS ARROWS' (U+1F500) browser- und systemabhängig dargestellt wird, wird statt dessen ein SVG verwendet. Als Hintergrundgrafik (wie im DOM-Tutorial) würde man zwei nahezu identische Grafiken benötigen, die sich nur durch die Füllfarbe unterscheiden. Als inline-SVG lässt sich der Button bequem mit CSS formatieren und kann so :hover und :focus sichtbar machen.
Autoplay-Modus
Die Funktion handleFullviewClickAndKeydown(event)
springt vom letzten Bild wieder zum ersten Bild und umgekehrt. Dies wollen wir mit dem play-Button zu einem automatischen Bildlauf verbinden. Dazu wird der Bildwechsel mit setInterval automatisch durchgeführt, bis wieder auf dem Play-Button geklickt wird.
Zufallswahl
Im Normalfall spielt der Player die playlist
in der Reihenfolge ab, die schon im Markup erscheint. Um über den Shuffle-Button eine zufällige Reihenfolge zu erreichen, wird mit Hilfe vom Math.random per Zufall das nächste Bild ermittelt. Dabei wird darauf geachtet, dass nicht zweimal hintereinander das selbe Bild gewählt wird. Der Shuffle-Betrieb wird durch erneuten Klick auf den Shuffle-Button beendet.
Zugänglichkeit
Um allen Besuchern die Bedienung zu ermöglichen und zu erleichtern, werden die Bedien-Buttons bei hover und focus farblich verändert.
Die Grafik des Play- und Shuffle-Buttons wird bei laufender Animation durch eine Pause-Grafik ersetzt.
Neben der Bedienung des Bildwechslers mit Maus und Touch und mit Tabulatortaste und Enter bzw. Space reagiert der Wechsler auch auf die Tasten "r", "s", "p", Pfeil-links, Pfeil-rechts, Escape, "x".
Um Maus- und Tastaturbenutzern eine Hilfe zu geben, haben die Buttons einen kurzen Hilfetext im title-Attribut. Dieser wird aber nur angezeigt, wenn sich die Maus über dem Button befindet, nicht aber bei Fokussierung mit dem Tabulator. Daher wird per css der Inhalt des title-Attributs in ein after-Pseudoelement kopiert.
Der fertige Komfort-Bildwechsler
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" media="screen" href="./Beispiel:SELFHTML-Beispiel-Grundlayout.css">
<title>Komfort-Bildwechsler</title>
<style>
.gallery {
& a {
text-decoration: none;
display: inline-block;
&:focus {
outline: 2px solid black;
}
& img {
width: 100px;
display: block;
}
}
}
#gallery-fullview {
width: 100vw; height: 100vh;
padding: 2em 5vw 8vh 5vw;
background-color: #888;
box-sizing: border-box;
border-radius: 10px;
&::backdrop {
background: rgb(200 200 200 /.8) ;
}
& figure {
border: thin solid #aaa;
margin: 0;
box-sizing: border-box;
width: 100%;
height: 100%;
background: #ccc;
}
& img {
display: block;
margin: auto;
width: 100%;
height: 100%;
object-fit: contain;
}
& figcaption {
font-style: italic;
text-align: center;
background-color: white;
}
& button {
position: absolute;
width: 2rem;
height: 2rem;
cursor: pointer;
border: 0;
background-color: transparent;
--fill: none;
--stroke: white;
&:focus {
outline: none;
}
&[value=close],
&[value=shuffle],
&[value=play] {
top: 0;
right: 0;
--fill: firebrick;
&:hover,
&:focus {
--fill: red;
}
}
&[value=shuffle],
&[value=play] {
& svg {
&:nth-of-type(1) {
display: inline;
}
&:nth-of-type(2) {
display: none;
}
}
}
&[value=shuffle] {
right: 4rem;
.shuffle & svg {
&:nth-of-type(1) {
display: none;
}
&:nth-of-type(2) {
display: inline;
}
}
}
&[value=play] {
right: 8rem;
.play & svg {
&:nth-of-type(1) {
display: none;
}
&:nth-of-type(2) {
display: inline;
}
}
}
&[value=prev],
&[value=next] {
top: calc(50% - 100px);
width: 100px;
height: 200px;
padding: 0;
--stroke: #fff6;
&:hover,
&:focus {
--stroke: skyblue;
}
}
&[value=prev] {
left: 2vw;
}
&[value=next] {
right: 2vw;
}
}
}
.visually-hidden {
position: absolute !important;
clip: rect(1px, 1px, 1px, 1px) !important;
padding: 0 !important;
border: 0 !important;
height: 1px !important;
width: 1px !important;
overflow: hidden !important;
white-space: nowrap !important;
}
</style>
<script>
'use strict';
document.addEventListener('DOMContentLoaded', function () {
// Zeit in ms zum betrachten der Bilder im Automatik-Modus
const zeitZumBetrachten = 1000;
// click-Handler auf alle Galerien legen. KÖNNTE man auch auf den Body legen und Bubbling nutzen, aber dann bekommt
// man mit currentTarget die Galerie nicht geschenkt (und muss sie vom target mit closest(''.gallery') suchen)
document.querySelectorAll(".gallery")
.forEach( gallery => gallery.addEventListener("click", handleGalleryClick) );
// EIGENEN click-Handler auf den Fullview.
document.getElementById("gallery-fullview")
.addEventListener("click", handleFullviewClickAndKeydown);
// EIGENEN keydown-Handler aufs Dokument.
document.addEventListener("keydown", handleFullviewClickAndKeydown);
// Klick in einer Galerie ist simpel, das behandelt alles die showInFullview Funktion,
// die auch vom handleFullviewClickAndKeydown zum navigieren verwendet wird.
function handleGalleryClick(event) {
// event.target ist das img Element oder welches HTML auch immer sonst im Thumb steckt. Suche
// das a Element als Bezugspunkt für die Fullview-Anzeige heraus
const clickedLink = event.target.closest("a");
// Nicht auf einen Link geklickt? Nichts tun.
if (!clickedLink) return;
event.preventDefault();
// Delegiere den Rest...
showInFullview(clickedLink);
}
function showInFullview(link) {
// der Fullscreen-Viewer
const fullview = document.getElementById("gallery-fullview"),
// die Galerie zum geklickten Link
gallery = link.closest(".gallery"),
// Das Thumbnail-Bild darin, brauchen wir für ...
thumb = link.querySelector("img"),
// ... den Caption-Text, der aus dem alt-Attribut generiert wird.
caption = thumb ? thumb.alt : "";
// Den angeklickten Link zum aria-selected Element machen
link.parentElement.querySelectorAll("a[aria-selected]").forEach(link => link.removeAttribute("aria-selected"));
link.setAttribute("aria-selected", "true");
// src-Attribut im Vollbild auf das verlinkte Bild setzen und Caption in figcaption eintragen
fullview.querySelector("img").src = link.href;
fullview.querySelector("figcaption").textContent = caption;
// Galerie-ID speichern für click-Handler im Fullview und Fullview einblenden (falls noch nicht passiert)
fullview.dataset.gallery = gallery.id;
fullview.showModal();
}
function handleFullviewClickAndKeydown(event) {
const fullview = document.getElementById("gallery-fullview")
if(!fullview) return;
const gallery = document.getElementById(fullview.dataset.gallery);
if(!gallery) return;
const currentThumb = gallery.querySelector("a[aria-selected]");
if (!currentThumb) return;
let nextThumb,action="",stopped;
if(event.type == "keydown") {
action = event.key; // Welche Taste wurde gedrückt?
}
if(event.type == "click") {
const clickedButton = event.target.closest("button");
// Nicht auf einen Button geklickt? Nichts tun.
if (!clickedButton) return;
action = clickedButton.value; // Welcher Button wurde geklickt?
}
switch (action) {
case 'close':
case 'Escape':
case 'x':
stopAnimations(fullview);
fullview.close();
fullview.dataset.gallery = null;
gallery.querySelector("a[aria-selected]").focus();
break;
case 'prev':
case 'ArrowLeft':
// Ausführliche Version
nextThumb = navigateDOM(currentThumb,
elem => elem.previousElementSibling,
elem => elem.tagName == "A");
if (!nextThumb)
nextThumb = gallery.querySelector("a:last-of-type")
showInFullview(nextThumb);
break;
case 'next':
case 'ArrowRight':
// Kompaktversion als Einzeiler
showInFullview(navigateDOM(currentThumb, elem => elem.nextElementSibling, elem => elem.tagName == "A") || gallery.querySelector("a:first-of-type"));
break;
case 'play':
case 'r':
// Evtl. laufende Animationen benden
stopped = stopAnimations(fullview);
if (!stopped.playStopped) {
// Mit setInterval die Bilder nacheinander zeigen (next animieren)
fullview.play = setInterval(function() {
showInFullview(navigateDOM(gallery.querySelector("a[aria-selected]"), elem => elem.nextElementSibling, elem => elem.tagName == "A") ||
gallery.querySelector("a:first-of-type"));
}, zeitZumBetrachten);
fullview.classList.add("play");
}
break;
case 'shuffle':
case 's':
// Evtl. laufende Animationen benden
stopped = stopAnimations(fullview);
if (!stopped.shuffleStopped) {
// Per Zufall das nächste Bilde ermitteln und anzeigen
const allThumbs = gallery.querySelectorAll("a");
let randomNumber,lastNumber=-1;
fullview.shuffle = setInterval(function() {
do {
randomNumber = Math.floor(Math.random()*allThumbs.length);
} while(randomNumber == lastNumber)
showInFullview(allThumbs[randomNumber]);
lastNumber = randomNumber;
}, zeitZumBetrachten);
fullview.classList.add("shuffle");
}
break;
case 'p':
// Evtl. laufende Animationen benden
stopAnimations(fullview);
break;
}
/* Helper: Navigiere schrittweise durch's DOM, bis eine Bedingung erfüllt ist
* "current" - Ausgangspunkt (ein HTML Elemnt)
* Was das "nächste" Thumbnail-Element ist, legt der proceed-Callback fest.
* Die Funktion sucht ausdrücklich nach a-Elementen - wenn andere Elemente im
* Container sind, werden sie übersprungen. Wird kein Link mehr gefunden, gibt
* die Funktion null zurück - um den Rest kümmere sich bitte der Aufrufer.
*/
function navigateDOM(current, proceed, checkFound) {
if (!current)
return null;
while (current = proceed(current)) {
if (checkFound(current))
break;
}
return current;
}
/* Helper: Beende evtl. laufende Animationen und gebe zurück,
* welche Animation beendet wurde. */
function stopAnimations(fullviewElement) {
let playStopped = false,
shuffleStopped = false;
if(fullview.play) {
clearInterval(fullviewElement.play);
fullviewElement.play = null;
fullviewElement.classList.remove("play");
playStopped = true;
}
if(fullviewElement.shuffle) {
clearInterval(fullviewElement.shuffle);
fullviewElement.shuffle = null;
fullviewElement.classList.remove("shuffle");
shuffleStopped = true;
}
return { "playStopped": playStopped, "shuffleStopped": shuffleStopped };
}
}
});
</script>
</head>
<body>
<h1>Komfort-Bildwechsler</h1>
<section id="peru" class="gallery">
<h2>Peru 2007</h2>
<a href="https://wiki.selfhtml.org/images/2/28/Peru-1.jpg">
<img src="https://wiki.selfhtml.org/images/2/24/Peru-1-sm.jpg" alt="Peru 2007: Cusco - Blick auf Ausangate">
</a>
<a href="https://wiki.selfhtml.org/images/4/42/Peru-2.jpg">
<img src="https://wiki.selfhtml.org/images/e/ea/Peru-2-sm.jpg" alt="Peru 2007: Valle Sagrado">
</a>
<a href="https://wiki.selfhtml.org/images/a/ab/Peru-3.jpg">
<img src="https://wiki.selfhtml.org/images/c/c5/Peru-3-sm.jpg" alt="Peru 2007: Machu Picchu">
</a>
<a href="https://wiki.selfhtml.org/images/8/80/Peru-4.jpg">
<img src="https://wiki.selfhtml.org/images/b/b0/Peru-4-sm.jpg" alt="Peru 2007: Machu Picchu - Lamas in den Ruinen">
</a>
<a href="https://wiki.selfhtml.org/images/3/3f/Peru-5.jpg">
<img src="https://wiki.selfhtml.org/images/7/71/Peru-5-sm.jpg" alt="Peru 2007: Uros-Inseln im Titicaca-See">
</a>
<a href="https://wiki.selfhtml.org/images/4/41/Peru-6.jpg">
<img src="https://wiki.selfhtml.org/images/5/5b/Peru-6-sm.jpg" alt="Peru 2007: Ceviche - Meeresfrüchte mit Zitronensaft">
</a>
</section>
<section id="gardasee" class="gallery">
<h2>Gardasee 2016</h2>
<a href="https://wiki.selfhtml.org/images/f/fb/Gardasee1.jpg">
<img src="https://wiki.selfhtml.org/images/8/8f/Gardasee1-sm.jpg" alt="Gardasee 2016: Sonnenuntergang bei Bardolino ">
</a>
<a href="https://wiki.selfhtml.org/images/1/16/Gardasee2.jpg">
<img src="https://wiki.selfhtml.org/images/6/67/Gardasee2-sm.jpg" alt="Gardasee 2016: Aperol Spritz - Entspannung für Lehrer ">
</a>
<a href="https://wiki.selfhtml.org/images/3/3b/Gardasee-3.jpg">
<img src="https://wiki.selfhtml.org/images/4/40/Gardasee-3-sm.jpg" alt="Gardasee 2016: Gardaland bei Bardolino - Blick von oben">
</a>
<a href="https://wiki.selfhtml.org/images/e/ef/Gardasee-4.jpg">
<img src="https://wiki.selfhtml.org/images/1/16/Gardasee-4-sm.jpg" alt="Gardasee 2016: Wasserrutsche im Gardaland ">
</a>
<a href="https://wiki.selfhtml.org/images/1/15/Gardasee-5.jpg">
<img src="https://wiki.selfhtml.org/images/c/c3/Gardasee-5-sm.jpg" alt="Gardasee 2016: Sonnenuntergang bei Bardolino">
</a>
<a href="https://wiki.selfhtml.org/images/5/5a/Gardasee-6.jpg">
<img src="https://wiki.selfhtml.org/images/d/d4/Gardasee-6-sm.jpg" alt="Gardasee 2016: nächtliches Handballspiel ">
</a>
<a href="https://wiki.selfhtml.org/images/2/2e/Gardasee-7.jpg">
<img src="https://wiki.selfhtml.org/images/a/a4/Gardasee-7-sm.jpg" alt="Gardasee 2016: Die antike Arena in Verona - heute für Konzerte und Opern genutzt ">
</a>
</section>
<p>Bei einem Klick auf die <em>Thumbnail</em>-Vorschau erhalten Sie eine Großansicht.</p>
<dialog id="gallery-fullview">
<figure>
<img alt="described by figcaption" src='data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg"%3E%3C/svg%3E'>
<figcaption></figcaption>
</figure>
<button type="button" value="close"><span class="visually-hidden">Schließen</span>
<svg viewBox='0 0 100 100'>
<rect width='100' height='100' fill='var(--fill)' />
<path d='M20,20 l60,60 m0,-60 l-60,60' fill='none' stroke='white' stroke-width='15' stroke-linecap='round'/>
</svg>
</button>
<button type="button" value="play" aria-label="play/pause animation"><span class="visually-hidden">automatisch abspielen</span>
<svg viewBox='0 0 200 200'>
<rect width='200' height='200' fill='var(--fill)' />
<path d='M60,30 l90,70 -90,70z' fill='white' stroke='white' stroke-width='30' stroke-linejoin='round' />
</svg>
<svg viewBox='0 0 100 100'>
<rect width='100' height='100' fill='var(--fill)' />
<path d='M30,20 L30,80 M70,20 L70,80' fill='none' stroke='white' stroke-width='25' stroke-linecap='round'/>
</svg>
</button>
<button type="button" value="shuffle"><span class="visually-hidden">shuffle</span>
<svg viewBox='0 0 376 376'>
<rect x='0' y='0' width='100%' height='100%' fill='var(--fill)' stroke='black' stroke-width='1'/>
<path d='M376 280l-79 68v-45h-13c-42 0-73-19-98-44 10-12 19-23 27-34 2-3 4-5 6-7 19 19 39 33 66 33h13v-38L376 280zM0 129h39c25 0 44 12 62 29 3-4 6-8 9-12 7-10 15-20 23-30 -25-23-55-40-95-40H0V129zM297 28v45h-13c-69 0-108 51-143 97 -31 41-58 76-101 76H0v53h39c69 0 108-51 143-97 31-41 58-76 101-76h13v38l79-68L297 28z' fill='white'/>
</svg>
<svg viewBox='0 0 100 100'>
<rect width='100' height='100' fill='var(--fill)' />
<path d='M30,20 L30,80 M70,20 L70,80' fill='none' stroke='white' stroke-width='25' stroke-linecap='round'/>
</svg>
</button>
<button type="button" value="prev"><span class="visually-hidden">vorheriges Bild</span>
<svg width='100' height='200' viewBox='0 0 100 200' xmlns='http://www.w3.org/2000/svg'>
<path id='left' d='M90,10 l-80,90 l80,90' fill='var(--fill)' stroke='var(--stroke)' stroke-width='15' stroke-linecap='round'/>
</svg>
</button>
<button type="button" value="next"><span class="visually-hidden">nächstes Bild</span>
<svg width='100' height='200' viewBox='0 0 100 200'>
<path id='right' d='M10,10 l80,90 l-80,90' fill='var(--fill)' stroke='var(--stroke)' stroke-width='15' stroke-linecap='round'/>
</svg>
</button>
</dialog>
</body>
</html>
In der Demo läuft die Animation im Sekundentakt, also zu schnell. In der ersten Zeile des Scripts kann diese Zeit über die Konstante zeitZumBetrachten
angepasst werden.