Einfaches Wischen mit Vanilla JavaScript

Avatar of Ana Tudor
Ana Tudor am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Früher dachte ich, die Implementierung von Wischgesten müsste sehr schwierig sein, aber kürzlich fand ich mich in einer Situation wieder, in der ich es tun musste, und entdeckte, dass die Realität lange nicht so düster ist, wie ich es mir vorgestellt hatte.

Dieser Artikel wird Sie Schritt für Schritt durch die Implementierung mit der geringstmöglichen Codezeile führen. Lassen Sie uns also direkt einsteigen!

Die HTML-Struktur

Wir beginnen mit einem .container, der eine Reihe von Bildern enthält.

<div class='container'>
  <img src='img1.jpg' alt='image description'/>
  ...
</div>

Grundlegende Stile

Wir verwenden display: flex, um sicherzustellen, dass die Bilder nebeneinander und ohne Abstände angezeigt werden. align-items: center richtet sie vertikal in der Mitte aus. Wir sorgen dafür, dass sowohl die Bilder als auch der Container die width des Elternelements des Containers (in unserem Fall body) einnehmen.

.container {
  display: flex;
  align-items: center;
  width: 100%;
  
  img {
    min-width: 100%; /* needed so Firefox doesn't make img shrink to fit */
    width: 100%; /* can't take this out either as it breaks Chrome */
  }
}

Die Tatsache, dass sowohl der .container als auch seine Kindbilder die gleiche width haben, bewirkt, dass diese Bilder auf der rechten Seite überlappen (wie durch den roten Umriss hervorgehoben), wodurch eine horizontale Scrollleiste entsteht, aber genau das wollen wir.

Screenshot showing this very basic layout with the container and the images having the same width as the body and the images spilling out of the container to the right, creating a horizontal scrollbar on the body.
Das anfängliche Layout (siehe Live-Demo).

Da nicht alle Bilder die gleichen Abmessungen und Seitenverhältnisse haben, gibt es über und unter einigen von ihnen etwas weißen Platz. Diesen werden wir abschneiden, indem wir dem .container eine explizite height geben, die für das durchschnittliche Seitenverhältnis dieser Bilder gut funktionieren sollte, und overflow-y auf hidden setzen.

.container {
  /* same as before */
  overflow-y: hidden;
  height: 50vw;
  max-height: 100vh;
}

Das Ergebnis ist unten zu sehen, wobei alle Bilder auf die gleiche height zugeschnitten sind und keine leeren Räume mehr vorhanden sind.

Screenshot showing the result after limiting the container's height and trimming everything that doesn't fit vertically with overflow-y. This means we now have a horizontal scrollbar on the container itself.
Das Ergebnis, nachdem die Bilder durch overflow-y auf dem .container zugeschnitten wurden (siehe Live-Demo).

Okay, aber jetzt haben wir eine horizontale Scrollleiste am .container selbst. Nun, das ist eigentlich gut für den Fall ohne JavaScript.

Andernfalls erstellen wir eine CSS-Variable --n für die Anzahl der Bilder und verwenden diese, um den .container breit genug zu machen, um alle seine Bildkinder aufzunehmen, die immer noch die gleiche Breite wie sein Elternelement (in diesem Fall das body) haben.

.container {
  --n: 1;
  width: 100%;
  width: calc(var(--n)*100%);
  
  img {
    min-width: 100%;
    width: 100%;
    width: calc(100%/var(--n));
  }
}

Beachten Sie, dass wir die vorherigen width-Deklarationen als Fallbacks beibehalten. Die calc()-Werte ändern nichts, bis wir --n über JavaScript festlegen, nachdem wir unseren .container und die Anzahl der darin enthaltenen Kindbilder ermittelt haben.

const _C = document.querySelector('.container'), 
      N = _C.children.length;

_C.style.setProperty('--n', N)

Jetzt hat sich unser .container erweitert, um alle Bilder darin unterzubringen.

Layout mit erweitertem Container (Live-Demo).

Bilder wechseln

Als Nächstes entfernen wir die horizontale Scrollleiste, indem wir overflow-x: hidden auf dem Elternelement unseres Containers (in unserem Fall body) setzen, und erstellen eine weitere CSS-Variable, die den Index des aktuell ausgewählten Bildes (--i) enthält. Wir verwenden dies, um den .container relativ zum Viewport durch eine Translation korrekt zu positionieren (denken Sie daran, dass %-Werte innerhalb von translate()-Funktionen sich auf die Abmessungen des Elements beziehen, auf das wir diese transform angewendet haben).

body { overflow-x: hidden }

.container {
  /* same styles as before */
  transform: translate(calc(var(--i, 0)/var(--n)*-100%));
}

Durch Ändern von --i auf einen anderen ganzzahligen Wert, der größer oder gleich Null, aber kleiner als --n ist, wird ein anderes Bild sichtbar, wie in der interaktiven Demo unten gezeigt (wobei der Wert von --i durch einen Bereichseingabefeld gesteuert wird).

Siehe den Pen von thebabydino (@thebabydino) auf CodePen.

Okay, aber wir wollen keinen Schieberegler dafür verwenden.

Die Grundidee ist, dass wir die Bewegungsrichtung zwischen dem "touchstart"- (oder "mousedown"-) Ereignis und dem "touchend"- (oder "mouseup"-) Ereignis erkennen und dann --i entsprechend aktualisieren, um den Container so zu verschieben, dass das nächste Bild (falls vorhanden) in der gewünschten Richtung in den Viewport bewegt wird.

function lock(e) {};

function move(e) {};

_C.addEventListener('mousedown', lock, false);
_C.addEventListener('touchstart', lock, false);

_C.addEventListener('mouseup', move, false);
_C.addEventListener('touchend', move, false);

Beachten Sie, dass dies für die Maus nur funktioniert, wenn wir pointer-events: none für die Bilder setzen.

.container {
  /* same styles as before */

  img {
    /* same styles as before */
    pointer-events: none;
  }
}

Außerdem muss Edge die Touch-Ereignisse ab about:flags aktiviert haben, da diese Option standardmäßig deaktiviert ist.

Screenshot showing the 'Enable touch events' option being set to 'Only when a touchscreen is detected' in about:flags in Edge.
Aktivieren von Touch-Ereignissen in Edge.

Bevor wir die Funktionen lock() und move() füllen, vereinheitlichen wir die Touch- und Klickfälle.

function unify(e) { return e.changedTouches ? e.changedTouches[0] : e };

Das Sperren bei "touchstart" (oder "mousedown") bedeutet, dass die x-Koordinate abgerufen und in einer anfänglichen Koordinatenvariable x0 gespeichert wird.

let x0 = null;

function lock(e) { x0 = unify(e).clientX };

Um zu sehen, wie wir unseren .container bewegen (oder ob wir das überhaupt tun, weil wir uns nicht weiter bewegen wollen, wenn wir das Ende erreicht haben), prüfen wir, ob wir die lock()-Aktion durchgeführt haben, und wenn ja, lesen wir die aktuelle x-Koordinate, berechnen die Differenz zwischen ihr und x0 und entscheiden basierend auf ihrem Vorzeichen und dem aktuellen Index, was zu tun ist.

let i = 0;

function move(e) {
  if(x0 || x0 === 0) {
    let dx = unify(e).clientX - x0, s = Math.sign(dx);
  
    if((i > 0 || s < 0) && (i < N - 1 || s > 0))
      _C.style.setProperty('--i', i -= s);
	
    x0 = null
  }
};

Das Ergebnis beim Ziehen nach links/rechts ist unten zu sehen.

Animated gif. Shows how we switch to the next image by dragging left/ right if there is a next image in the direction we want to go. Attempts to move to the right on the first image or left on the last one do nothing as we have no other image before or after, respectively.
Umschalten zwischen Bildern per Wischgeste (Live-Demo). Versuche, beim ersten Bild nach rechts oder beim letzten Bild nach links zu wechseln, führen zu nichts, da wir davor oder danach keine weiteren Bilder haben.

Das Obige ist das erwartete Ergebnis und das Ergebnis, das wir in Chrome für ein wenig Ziehen und in Firefox erhalten. Edge navigiert jedoch beim Ziehen nach links oder rechts vorwärts und rückwärts, was Chrome ebenfalls bei etwas mehr Ziehen tut.

Animated gif. Shows how Edge navigates the pageview backward and forward when we swipe left or right.
Edge navigiert beim Wischen nach links oder rechts die Seitenansicht vorwärts oder rückwärts.

Um dies zu überschreiben, müssen wir einen "touchmove"-Ereignis-Listener hinzufügen.

_C.addEventListener('touchmove', e => {e.preventDefault()}, false)

Okay, wir haben jetzt etwas Funktionierendes in allen Browsern, aber es sieht noch nicht so aus, wie wir es wirklich wollen ... noch!

Flüssige Bewegung

Der einfachste Weg, dem Ziel näher zu kommen, ist das Hinzufügen einer transition.

.container {
  /* same styles as before */
  transition: transform .5s ease-out;
}

Und hier ist es: ein sehr einfacher Wisch-Effekt in etwa 25 Zeilen JavaScript und etwa 25 Zeilen CSS.

Funktionierender Wisch-Effekt (Live-Demo).

Leider gibt es einen Edge-Bug, der dazu führt, dass jede transition zu einer auf CSS-Variablen basierenden calc()-Translation fehlschlägt. Ugh, ich schätze, wir müssen Edge vorerst vergessen.

Das Ganze verfeinern

Mit all den coolen Wisch-Effekten da draußen ist das, was wir bisher haben, nicht ganz ausreichend. Mal sehen, welche Verbesserungen wir machen können.

Bessere visuelle Hinweise während des Ziehens

Zuerst passiert nichts, während wir ziehen, alle Aktionen folgen dem "touchend"- (oder "mouseup"-) Ereignis. Also, während wir ziehen, haben wir keine Ahnung, was als nächstes passieren wird. Gibt es ein nächstes Bild, zu dem in der gewünschten Richtung gewechselt werden kann? Oder haben wir das Ende der Fahnenstange erreicht und es wird nichts passieren?

Um uns darum zu kümmern, passen wir den Translationsbetrag etwas an, indem wir eine CSS-Variable --tx hinzufügen, die ursprünglich 0px ist.

transform: translate(calc(var(--i, 0)/var(--n)*-100% + var(--tx, 0px)))

Wir verwenden zwei weitere Ereignis-Listener: einen für "touchmove" und einen für "mousemove". Beachten Sie, dass wir die Rückwärts- und Vorwärtsnavigation in Chrome bereits mit dem "touchmove"-Listener verhindert haben.

function drag(e) { e.preventDefault() };

_C.addEventListener('mousemove', drag, false);
_C.addEventListener('touchmove', drag, false);

Jetzt füllen wir die drag()-Funktion! Wenn wir die lock()-Aktion durchgeführt haben, lesen wir die aktuelle x-Koordinate, berechnen die Differenz dx zwischen dieser Koordinate und der anfänglichen x0 und setzen --tx auf diesen Wert (der ein Pixelwert ist).

function drag(e) {
  e.preventDefault();

  if(x0 || x0 === 0)  
    _C.style.setProperty('--tx', `${Math.round(unify(e).clientX - x0)}px`)
};

Wir müssen auch sicherstellen, dass --tx am Ende auf 0px zurückgesetzt wird und die transition für die Dauer des Ziehens entfernt wird. Um dies zu erleichtern, verschieben wir die transition-Deklaration in eine .smooth-Klasse.

.smooth { transition: transform .5s ease-out; }

In der lock()-Funktion entfernen wir diese Klasse vom .container (wir fügen sie am Ende bei "touchend" und "mouseup" wieder hinzu) und setzen auch eine boolesche Variable locked, damit wir nicht ständig die x0 || x0 === 0-Prüfung durchführen müssen. Stattdessen verwenden wir die locked-Variable für die Prüfungen.

let locked = false;

function lock(e) {
  x0 = unify(e).clientX;
  _C.classList.toggle('smooth', !(locked = true))
};

function drag(e) {
  e.preventDefault();
  if(locked) { /* same as before */ }
};

function move(e) {
  if(locked) {
    let dx = unify(e).clientX - x0, s = Math.sign(dx);

    if((i > 0 || s < 0) && (i < N - 1 || s > 0))
    _C.style.setProperty('--i', i -= s);
    _C.style.setProperty('--tx', '0px');
    _C.classList.toggle('smooth', !(locked = false));
    x0 = null
  }
};

Das Ergebnis ist unten zu sehen. Während wir noch ziehen, haben wir jetzt eine visuelle Anzeige dessen, was als nächstes passieren wird.

Wischen mit visuellen Hinweisen während des Ziehens (Live-Demo).

transition-duration reparieren

Zu diesem Zeitpunkt verwenden wir immer die gleiche transition-duration, unabhängig davon, wie viel der width eines Bildes wir nach dem Ziehen noch übersetzen müssen. Das können wir auf ziemlich einfache Weise beheben, indem wir einen Faktor f einführen, den wir auch als CSS-Variable festlegen, um uns bei der Berechnung der tatsächlichen Animationsdauer zu helfen.

.smooth { transition: transform calc(var(--f, 1)*.5s) ease-out; }

Im JavaScript ermitteln wir die width eines Bildes (aktualisiert bei "resize") und berechnen, für welchen Bruchteil davon wir horizontal gezogen haben.

let w;

function size() { w = window.innerWidth };

function move(e) {
  if(locked) {
    let dx = unify(e).clientX - x0, s = Math.sign(dx), 
        f = +(s*dx/w).toFixed(2);

    if((i > 0 || s < 0) && (i < N - 1 || s > 0)) {
      _C.style.setProperty('--i', i -= s);
      f = 1 - f
    }
		
    _C.style.setProperty('--tx', '0px');
    _C.style.setProperty('--f', f);
    _C.classList.toggle('smooth', !(locked = false));
    x0 = null
  }
};

size();

addEventListener('resize', size, false);

Dies ergibt jetzt ein besseres Ergebnis.

Zurückspringen bei unzureichendem Ziehen

Nehmen wir an, wir wollen nicht zum nächsten Bild wechseln, wenn wir nur ein wenig unter einem bestimmten Schwellenwert ziehen. Denn jetzt bedeutet eine 1px-Differenz während des Ziehens, dass wir zum nächsten Bild wechseln, und das fühlt sich etwas unnatürlich an.

Um dies zu beheben, legen wir einen Schwellenwert fest, sagen wir 20% der width eines Bildes.

function move(e) {
  if(locked) {
    let dx = unify(e).clientX - x0, s = Math.sign(dx), 
        f = +(s*dx/w).toFixed(2);

    if((i > 0 || s < 0) && (i < N - 1 || s > 0) && f > .2) {
      /* same as before */
    }
		
    /* same as before */
  }
};

Das Ergebnis ist unten zu sehen.

Wir wechseln nur zum nächsten Bild, wenn wir genug gezogen haben (Live-Demo).

Vielleicht einen Sprung hinzufügen?

Das ist etwas, von dem ich nicht sicher bin, ob es eine gute Idee war, aber ich musste es sowieso ausprobieren: Ändern Sie die Timing-Funktion, um einen Sprung einzuführen. Nach einigem Ziehen an den Griffen auf cubic-bezier.com kam ich zu einem Ergebnis, das vielversprechend aussah.

Animated gif. Shows the graphical representation of the cubic Bézier curve, with start point at (0, 0), end point at (1, 1) and control points at (1, 1.59) and (.61, .74), the progression on the [0, 1] interval being a function of time in the [0, 1] interval. Also illustrates how the transition function given by this cubic Bézier curve looks when applied on a translation compared to a plain ease-out.
Was unsere gewählte kubische Bézier-Timing-Funktion im Vergleich zu einem einfachen ease-out aussieht.
transition: transform calc(var(--f)*.5s) cubic-bezier(1, 1.59, .61, .74);
Verwendung einer benutzerdefinierten CSS-Timing-Funktion zur Einführung eines Sprungs (Live-Demo).

Wie wäre es mit dem JavaScript-Weg?

Wir könnten einen besseren Grad an Kontrolle über natürlichere und komplexere Sprünge erreichen, indem wir den JavaScript-Weg für die Transition wählen. Das würde auch Edge-Unterstützung bieten.

Wir beginnen damit, die transition und die CSS-Variablen --tx und --f zu entfernen. Dies reduziert unser transform auf das, was es ursprünglich war.

transform: translate(calc(var(--i, 0)/var(--n)*-100%));

Der obige Code bedeutet auch, dass --i nicht mehr unbedingt eine Ganzzahl ist. Während es eine Ganzzahl bleibt, wenn wir ein einzelnes Bild vollständig im Blick haben, ist das nicht mehr der Fall, während wir ziehen oder während der Bewegung nach Auslösen der "touchend"- oder "mouseup"-Ereignisse.

Annotated screenshots illustrating what images we see for --i: 0 (1st image), --i: 1 (2nd image), --i: .5 (half of 1st and half of 2nd) and --i: .75 (a quarter of 1st and three quarters of 2nd).
Zum Beispiel, während wir das erste Bild vollständig im Blick haben, ist --i 0. Während wir das zweite vollständig im Blick haben, ist --i 1. Wenn wir uns auf halbem Weg zwischen dem ersten und dem zweiten befinden, ist --i .5. Wenn wir ein Viertel des ersten und drei Viertel des zweiten im Blick haben, ist --i .75.

Wir aktualisieren dann das JavaScript, um die Code-Teile zu ersetzen, in denen wir diese CSS-Variablen aktualisiert haben. Zuerst kümmern wir uns um die lock()-Funktion, wo wir das Umschalten der .smooth-Klasse aufgeben, und um die drag()-Funktion, wo wir die Aktualisierung der verworfenen --tx-Variable durch die Aktualisierung von --i ersetzen, das, wie bereits erwähnt, keine Ganzzahl mehr sein muss.

function lock(e) {
  x0 = unify(e).clientX;
  locked = true
};

function drag(e) {
  e.preventDefault();
	
  if(locked) {
    let dx = unify(e).clientX - x0, 
      f = +(dx/w).toFixed(2);
		
    _C.style.setProperty('--i', i - f)
  }
};

Bevor wir auch die move()-Funktion aktualisieren, führen wir zwei neue Variablen ein: ini und fin. Diese repräsentieren den Anfangswert, den wir --i zu Beginn der Animation zuweisen, und den Endwert, den wir derselben Variablen am Ende der Animation zuweisen. Wir erstellen auch eine Animationsfunktion ani().

let ini, fin;

function ani() {};

function move(e) {
  if(locked) {
    let dx = unify(e).clientX - x0, 
        s = Math.sign(dx), 
        f = +(s*dx/w).toFixed(2);
		
    ini = i - s*f;

    if((i > 0 || s < 0) && (i < N - 1 || s > 0) && f > .2) {
      i -= s;
      f = 1 - f
    }

    fin = i;
    ani();
    x0 = null;
    locked = false;
  }
};

Das ist nicht viel anders als der bisherige Code. Geändert hat sich, dass wir in dieser Funktion keine CSS-Variablen mehr setzen, sondern stattdessen die JavaScript-Variablen ini und fin setzen und die Animationsfunktion ani() aufrufen.

ini ist der Anfangswert, den wir --i zu Beginn der Animation zuweisen, die durch das "touchend"/"mouseup"-Ereignis ausgelöst wird. Dieser ergibt sich aus der aktuellen Position, die wir haben, wenn eines dieser beiden Ereignisse eintritt.

fin ist der Endwert, den wir --i am Ende derselben Animation zuweisen. Dies ist immer ein ganzzahliger Wert, da wir immer mit einem vollständig sichtbaren Bild enden, sodass fin und --i der Index dieses Bildes sind. Dies ist das nächste Bild in der gewünschten Richtung, wenn wir genug gezogen haben (f > .2) und wenn es ein nächstes Bild in der gewünschten Richtung gibt ((i > 0 || s < 0) && (i < N - 1 || s > 0)). In diesem Fall aktualisieren wir auch die JavaScript-Variable, die den aktuellen Bildindex (i) und den relativen Abstand dazu (f) speichert. Andernfalls ist es dasselbe Bild, sodass i und f nicht aktualisiert werden müssen.

Kommen wir nun zur ani()-Funktion. Wir beginnen mit einer vereinfachten linearen Version, die eine Richtungsänderung auslässt.

const NF = 30;

let rID = null;

function stopAni() {
  cancelAnimationFrame(rID);
  rID = null
};

function ani(cf = 0) {
  _C.style.setProperty('--i', ini + (fin - ini)*cf/NF);
	
  if(cf === NF) {
    stopAni();
    return
  }
	
  rID = requestAnimationFrame(ani.bind(this, ++cf))
};

Die Hauptidee hierbei ist, dass der Übergang vom Anfangswert ini zum Endwert fin über eine Gesamtzahl von Frames NF erfolgt. Jedes Mal, wenn wir die ani()-Funktion aufrufen, berechnen wir den Fortschritt als Verhältnis zwischen dem aktuellen Frame-Index cf und der Gesamtzahl der Frames NF. Dies ist immer eine Zahl zwischen 0 und 1 (oder Sie können sie als Prozentsatz betrachten, von 0% bis 100%). Wir verwenden diesen Fortschrittswert dann, um den aktuellen Wert von --i zu erhalten und ihn im Stilattribut unseres Containers _C festzulegen. Wenn wir den Endzustand erreicht haben (der aktuelle Frame-Index cf entspricht der Gesamtzahl der Frames NF), verlassen wir die Animationsschleife. Andernfalls erhöhen wir einfach den aktuellen Frame-Index cf und rufen ani() erneut auf.

Zu diesem Zeitpunkt haben wir eine funktionierende Demo mit einer linearen JavaScript-Transition.

Version mit linearer JavaScript-Transition (Live-Demo).

Dies hat jedoch das Problem, das wir ursprünglich im CSS-Fall hatten: Unabhängig von der Distanz müssen wir beim Loslassen ("touchend" / "mouseup") unser Element glatt verschieben, und die Dauer ist immer gleich, da wir immer über die gleiche Anzahl von Frames NF animieren.

Lasst uns das beheben!

Um dies zu erreichen, führen wir eine weitere Variable anf ein, in der wir die tatsächliche Anzahl der verwendeten Frames speichern und deren Wert wir in der move()-Funktion berechnen, bevor wir die Animationsfunktion ani() aufrufen.

function move(e) {
  if(locked) {
    let dx = unify(e).clientX - x0, 
      s = Math.sign(dx), 
      f = +(s*dx/w).toFixed(2);
		
    /* same as before */

    anf = Math.round(f*NF);
    ani();

    /* same as before */
  }
};

Wir müssen auch NF durch anf in der Animationsfunktion ani() ersetzen.

function ani(cf = 0) {
  _C.style.setProperty('--i', ini + (fin - ini)*cf/anf);
	
  if(cf === anf) { /* same as before */ }
	
  /* same as before */
};

Damit haben wir das Timing-Problem behoben!

Version mit linearer JavaScript-Transition bei konstanter Geschwindigkeit (Live-Demo).

Okay, aber eine lineare Timing-Funktion ist nicht gerade aufregend.

Wir könnten die JavaScript-Äquivalente von CSS-Timing-Funktionen wie ease-in, ease-out oder ease-in-out ausprobieren und sehen, wie sie sich vergleichen. Ich habe bereits im zuvor verlinkten Artikel im Detail erklärt, wie man diese erhält, daher werde ich nicht noch einmal darauf eingehen und das Objekt mit allen davon einfach in den Code fallen lassen.

const TFN = {
  'linear': function(k) { return k }, 
  'ease-in': function(k, e = 1.675) {
    return Math.pow(k, e)
  }, 
  'ease-out': function(k, e = 1.675) {
    return 1 - Math.pow(1 - k, e)
  }, 
  'ease-in-out': function(k) {
    return .5*(Math.sin((k - .5)*Math.PI) + 1)
  }
};

Der k-Wert ist der Fortschritt, der das Verhältnis zwischen dem aktuellen Frame-Index cf und der tatsächlichen Anzahl von Frames, über die die Transition läuft, anf ist. Das bedeutet, wir modifizieren die ani()-Funktion ein wenig, wenn wir zum Beispiel die Option ease-out verwenden wollen.

function ani(cf = 0) {
  _C.style.setProperty('--i', ini + (fin - ini)*TFN['ease-out'](cf/anf));
	
  /* same as before */
};
Version mit ease-out JavaScript-Transition (Live-Demo).

Wir könnten die Dinge auch interessanter machen, indem wir die Art von hüpfender Timing-Funktion verwenden, die CSS nicht bieten kann. Zum Beispiel etwas Ähnliches wie das, was in der unten gezeigten Demo illustriert wird (klicken, um eine Transition auszulösen).

Siehe den Pen von thebabydino (@thebabydino) auf CodePen.

Die Grafik dafür wäre der des easeOutBounce Timing-Funktions-Typs von easings.net ähnlich.

Animated gif. Shows the graph of the bouncing timing function. This function has a slow, then accelerated increase from the initial value to its final value. Once it reaches the final value, it quickly bounces back by about a quarter of the distance between the final and initial value, then going back to the final value, again bouncing back a bit. In total, it bounces three times. On the right side, we have an animation of how the function value (the ordinate on the graph) changes in time (as we progress along the abscissa).
Grafische Darstellung der Timing-Funktion.

Der Prozess, diese Art von Timing-Funktion zu erhalten, ähnelt dem Erhalt der JavaScript-Version der CSS ease-in-out (wiederum beschrieben im zuvor verlinkten Artikel über die Emulation von CSS-Timing-Funktionen mit JavaScript).

Wir beginnen mit der Kosinusfunktion im Intervall [0, 90°] (oder [0, π/2] in Radiant) für keinen Sprung, [0, 270°] ([0, 3·π/2]) für 1 Sprung, [0, 450°] ([0, 5·π/2]) für 2 Sprünge und so weiter ... generell ist es das Intervall [0, (n + ½)·180°] ([0, (n + ½)·π]) für n Sprünge.

Siehe den Pen von thebabydino (@thebabydino) auf CodePen.

Die Eingabe dieser cos(k)-Funktion liegt im Intervall [0, 450°], während ihre Ausgabe im Intervall [-1, 1] liegt. Wir wollen jedoch eine Funktion, deren Definitionsbereich das Intervall [0, 1] ist und deren Wertebereich ebenfalls das Intervall [0, 1] ist.

Wir können den Wertebereich auf das Intervall [0, 1] beschränken, indem wir nur den Absolutwert |cos(k)| nehmen.

Siehe den Pen von thebabydino (@thebabydino) auf CodePen.

Während wir das gewünschte Intervall für den Wertebereich erhalten haben, wollen wir, dass der Wert dieser Funktion bei 0 0 ist und ihr Wert am anderen Ende des Intervalls 1 ist. Aktuell ist es umgekehrt, aber das können wir beheben, indem wir unsere Funktion zu 1 - |cos(k)| ändern.

Siehe den Pen von thebabydino (@thebabydino) auf CodePen.

Nun können wir den Definitionsbereich vom Intervall [0, (n + ½)·180°] auf das Intervall [0, 1] beschränken. Um dies zu tun, ändern wir unsere Funktion zu 1 - |cos(k·(n + ½)·180°)|.

Siehe den Pen von thebabydino (@thebabydino) auf CodePen.

Dies gibt uns sowohl den gewünschten Definitionsbereich als auch den Wertebereich, aber wir haben immer noch einige Probleme.

Erstens haben alle unsere Sprünge die gleiche Höhe, aber wir möchten, dass ihre Höhe abnimmt, wenn k von 0 bis 1 ansteigt. Unsere Lösung in diesem Fall ist, den Kosinus mit 1 - k (oder mit einer Potenz von 1 - k für eine nichtlineare Amplitudenabnahme) zu multiplizieren. Die interaktive Demo unten zeigt, wie sich diese Amplitude für verschiedene Exponenten a ändert und wie dies die bisherige Funktion beeinflusst.

Siehe den Pen von thebabydino (@thebabydino) auf CodePen.

Zweitens nehmen alle Sprünge die gleiche Zeit in Anspruch, obwohl ihre Amplituden weiter abnehmen. Die erste Idee hier ist, eine Potenz von k anstelle von nur k in der Kosinusfunktion zu verwenden. Dies macht die Dinge seltsam, da der Kosinus nicht mehr in gleichen Intervallen 0 erreicht, was bedeutet, dass wir nicht immer f(1) = 1 erhalten, was wir jedoch immer von einer Timing-Funktion benötigen, die wir tatsächlich verwenden wollen. Für etwas wie a = 2.75, n = 3 und b = 1.5 erhalten wir jedoch ein zufriedenstellendes Ergebnis, also belassen wir es dabei, auch wenn es für bessere Kontrolle optimiert werden könnte.

Screenshot of the previously linked demo showing the graphical result of the a = 2.75, n = 3 and b = 1.5 setup: a slow, then fast increase from 0 (for f(0)) to 1, bouncing back down less than half the way after reaching 1, going back up and then having another even smaller bounce before finishing at 1, where we always want to finish for f(1).
Die Timing-Funktion, die wir ausprobieren wollen.

Dies ist die Funktion, die wir im JavaScript ausprobieren, wenn wir möchten, dass etwas springt.

const TFN = {
  /* the other function we had before */
  'bounce-out': function(k, n = 3, a = 2.75, b = 1.5) {
    return 1 - Math.pow(1 - k, a)*Math.abs(Math.cos(Math.pow(k, b)*(n + .5)*Math.PI))
  }
};

Hmm, das scheint in der Praxis etwas zu extrem.

Version mit einer springenden JavaScript-Transition (Live-Demo).

Vielleicht könnten wir n vom Betrag der Verdrängung abhängig machen, die wir ab dem Zeitpunkt des Loslassens noch ausführen müssen. Wir machen daraus eine Variable, die wir dann in der move()-Funktion festlegen, bevor wir die Animationsfunktion ani() aufrufen.

const TFN = {
  /* the other function we had before */
  'bounce-out': function(k, a = 2.75, b = 1.5) {
    return 1 - Math.pow(1 - k, a)*Math.abs(Math.cos(Math.pow(k, b)*(n + .5)*Math.PI))
  }
};

var n;

function move(e) {
  if(locked) {
    let dx = unify(e).clientX - x0, 
      s = Math.sign(dx), 
      f = +(s*dx/w).toFixed(2);
    
    /* same as before */
		
    n = 2 + Math.round(f)
    ani();
    /* same as before */
  }
};

Dies ergibt unser Endergebnis.

Version mit der endgültigen springenden JavaScript-Transition (Live-Demo).

Es gibt definitiv noch Raum für Verbesserungen, aber ich habe kein Gespür dafür, was eine gute Animation ausmacht, also belasse ich es dabei. So wie es ist, ist es jetzt browserübergreifend funktionsfähig (ohne die Edge-Probleme, die die Version mit CSS-Transition hat) und ziemlich flexibel.