1 HTML Element + 5 CSS-Eigenschaften = Magie!

Avatar of Ana Tudor
Ana Tudor am

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

Nehmen wir an, ich würde Ihnen sagen, dass wir die unten stehenden Ergebnisse mit nur einem HTML-Element und jeweils fünf CSS-Eigenschaften erzielen können. Kein SVG, keine Bilder (außer dem background am Root, das nur dazu dient, deutlich zu machen, dass unser einziges HTML-Element einige transparente Teile hat), kein JavaScript. Was würden Sie denken, was das beinhaltet?

Screenshots. On the left, a screenshot of equal radial slices of a pie with transparent slices (gaps) in between them. The whole assembly has a top to bottom gradient (orange to purple). On the right, the XOR operation between what we have on the left and a bunch of concentric ripples. Again, the whole assembly has the same top to bottom gradient.
Die gewünschten Ergebnisse.

Nun, dieser Artikel wird erklären, wie das geht, und dann auch zeigen, wie man Dinge durch Hinzufügen von Animationen unterhaltsam gestaltet.

CSS-ing the Gradient Rays

Der HTML-Code besteht nur aus einem <div>.

<div class='rays'></div>

Im CSS müssen wir die Abmessungen dieses Elements festlegen und ihm einen background geben, damit wir es sehen können. Wir machen es auch rund mit border-radius.

.rays {
  width: 80vmin; height: 80vmin;
  border-radius: 50%;
  background: linear-gradient(#b53, #f90);
}

Und… wir haben bereits vier der fünf Eigenschaften verwendet, um das folgende Ergebnis zu erzielen.

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

Was ist die fünfte? mask mit einem Wert von repeating-conic-gradient()!

Nehmen wir an, wir wollen 20 Strahlen haben. Das bedeutet, wir müssen $p: 100%/20 des vollen Kreises für einen Strahl und die Lücke danach zuweisen.

Illustration. Shows how we slice the disc to divide it into equal rays and gaps.
Teilung der Scheibe in Strahlen und Lücken (live).

Hier halten wir die Lücken zwischen den Strahlen gleich den Strahlen (also .5*$p für einen Strahl oder einen Leerraum), aber wir können beides breiter oder schmaler machen. Wir wollen eine abrupte Änderung nach der Endposition des undurchsichtigen Teils (des Strahls), daher sollte die Startposition für den transparenten Teil (die Lücke) gleich oder kleiner als diese sein. Wenn also die Endposition für den Strahl .5*$p ist, kann die Startposition für die Lücke nicht größer sein. Sie kann jedoch kleiner sein, und das hilft uns, die Dinge einfach zu halten, da wir sie einfach auf Null setzen können.

SVG illustration. Connects the stop positions from the code to the actual corresponding points on the circle defining the repeating conic gradient.
Wie repeating-conic-gradient() funktioniert (live).
$nr: 20; // number of rays
$p: 100%/$nr; // percent of circle allocated to a ray and gap after

.rays {
  /* same as before */
  mask: repeating-conic-gradient(#000 0% .5*$p, transparent 0% $p);
}

Beachten Sie, dass im Gegensatz zu linearen und radialen Verläufen die Stopp-Positionen für konische Verläufe keine Einheiten haben können. Sie müssen entweder Prozentsätze oder Winkelwerte sein. Das bedeutet, dass die Verwendung von etwas wie transparent 0 $p nicht funktioniert. Wir brauchen transparent 0% $p (oder 0deg anstelle von 0%; es spielt keine Rolle, welche wir wählen, sie kann einfach nicht ohne Einheit sein).

Screenshot of equal radial slices of a pie with transparent slices (gaps) in between them. The whole assembly has a top to bottom gradient (orange to purple).
Gradientenstrahlen (Live-Demo, keine Edge-Unterstützung).

Hier gibt es ein paar Dinge zu beachten, wenn es um die Unterstützung geht.

  • Edge unterstützt derzeit keine Maskierung auf HTML-Elementen, obwohl dies als In Development aufgeführt ist und ein Flag dafür (das vorerst nichts bewirkt) bereits in about:flags aufgetaucht ist.
    Screenshot showing the about:flags page in Edge, with the 'Enable CSS Masking' flag highlighted.
    Das Flag Enable CSS Masking in Edge.
  • conic-gradient() wird nativ nur von Blink-Browsern hinter dem Flag Experimental Web Platform features unterstützt (das über chrome://flags oder opera://flags aktiviert werden kann). Unterstützung kommt auch für Safari, aber bis dahin ist Safari immer noch auf das Polyfill angewiesen, genau wie Firefox (oder Edge, wenn es ab der nächsten Version auch Maskierung unterstützt). Update: Ab Chrome 69 ist conic-gradient() nicht mehr hinter einem Flag – es funktioniert jetzt in jedem aktuellen Blink-Browser, unabhängig davon, ob das Flag aktiviert ist oder nicht.
    Screenshot showing the Experimental Web Platform Features flag being enabled in Chrome.
    Das Flag Experimental Web Platform features ist in Chrome aktiviert.
  • WebKit-Browser benötigen für mask-Eigenschaften bei HTML-Elementen immer noch das Präfix -webkit-. Man könnte denken, das sei kein Problem, da wir das Polyfill verwenden, das ohnehin auf -prefix-free basiert. Wenn wir also das Polyfill verwenden, müssen wir -prefix-free sowieso davor einbinden. Leider ist es etwas komplizierter als das. Das liegt daran, dass -prefix-free über Feature-Erkennung funktioniert, was in diesem Fall fehlschlägt, da alle Browser mask ohne Präfix unterstützen… auf SVG-Elementen! Aber wir verwenden hier mask auf einem HTML-Element, daher sind wir in der Situation, dass WebKit-Browser das Präfix -webkit- benötigen, -prefix-free es aber nicht hinzufügt. Ich denke also, wir müssen es manuell hinzufügen.
    $nr: 20; // number of rays
    $p: 100%/$nr; // percent of circle allocated to a ray and gap after
    $m: repeating-conic-gradient(#000 0% .5*$p, transparent 0% $p); // mask
    
    .rays {
      /* same as before */
      -webkit-mask: $m;
              mask: $m;
    }

    Ich denke, wir könnten auch Autoprefixer verwenden, auch wenn wir -prefix-free sowieso einbinden müssen, aber nur dafür beides zu verwenden, fühlt sich ein wenig wie mit Kanonen auf Spatzen schießen an.

Hinzufügen von Animationen

Eine coole Sache an conic-gradient(), das nativ in Blink-Browsern unterstützt wird, ist, dass wir CSS-Variablen darin verwenden können (das geht nicht, wenn wir das Polyfill verwenden). Und CSS-Variablen können jetzt auch in Blink-Browsern mit ein wenig Houdini-Magie animiert werden (wir benötigen das Flag Experimental Web Platform features dafür, auch wenn wir es für die native conic-gradient()-Unterstützung ab Chrome 69+ nicht mehr benötigen).

Um unseren Code für die Animation vorzubereiten, ändern wir unseren Maskierungsgradienten so, dass er variable Alpha-Werte verwendet.

$m: repeating-conic-gradient(
      rgba(#000, var(--a)) 0% .5*$p, 
      rgba(#000, calc(1 - var(--a))) 0% $p);

Dann registrieren wir die benutzerdefinierte Eigenschaft für die Alpha --a.

CSS.registerProperty({
  name: '--a', 
  syntax: '<number>', 
  initialValue: 1, 
  inherits: true
})

Beachten Sie, dass die Spezifikation jetzt verlangt, dass inherits explizit angegeben wird, obwohl es vorher optional war. Wenn also Houdini-Demos, die dies nicht angeben, kaputt sind, ist dies mindestens einer der Gründe dafür.

Und schließlich fügen wir im CSS eine animation hinzu.

.rays {
  /* same as before */
  animation: a 2s linear infinite alternate;
}

@keyframes a { to { --a: 0 } }

Dies ergibt das folgende Ergebnis.

Animated gif. We animate the alpha of the gradient stops, such that the rays go from fully opaque to fully transparent, effectively becoming gaps, while the opposite happens for the initial gaps, they go from fully transparent to fully opaque, thus becoming rays. At any moment, the alpha of either of them is  1 minus the alpha of the other, so they complement each other.
Alpha-Animation der Strahlen (Live-Demo, funktioniert nur in Blink-Browsern mit aktiviertem Flag Experimental Web Platform features).

Meh. Sieht nicht so toll aus. Wir könnten es aber interessanter machen, indem wir mehrere Alpha-Werte verwenden.

$m: repeating-conic-gradient(
      rgba(#000, var(--a0)) 0%, rgba(#000, var(--a1)) .5*$p, 
      rgba(#000, var(--a2)) 0%, rgba(#000, var(--a3)) $p);

Der nächste Schritt ist die Registrierung jeder dieser benutzerdefinierten Eigenschaften.

for(let i = 0; i < 4; i++) {
  CSS.registerProperty({
    name: `--a${i}`, 
    syntax: '<number>', 
    initialValue: 1 - ~~(i/2), 
    inherits: true
  })
}

Und schließlich die Animationen im CSS hinzufügen.

.rays {
  /* same as before */
  animation: a 2s infinite alternate;
  animation-name: a0, a1, a2, a3;
  animation-timing-function: 
    /* easings from easings.net */
    cubic-bezier(.57, .05, .67, .19) /* easeInCubic */, 
    cubic-bezier(.21, .61, .35, 1); /* easeOutCubic */
}

@for $i from 0 to 4 {
  @keyframes a#{$i} { to { --a#{$i}: #{floor($i/2)} } }
}

Beachten Sie, dass wir, da wir Werte für benutzerdefinierte Eigenschaften festlegen, die Funktion floor() interpolieren müssen.

Animated gif. This time, the alpha of each and every stop (start and end of ray, start and end of gap) is animated independently via its own CSS variable. The alphas at the start and end of the ray both go from 1 to 0, but using different timing functions. The alphas at the start and end of the gap both go from 0 to 1, but, again, using different timing functions.
Animationen mehrerer Strahlen-Alphas (Live-Demo, funktioniert nur in Blink-Browsern mit aktiviertem Flag Experimental Web Platform features).

Sieht jetzt etwas interessanter aus, aber wir können es sicher noch besser machen?

Versuchen wir, eine CSS-Variable für die Stopp-Position zwischen dem Strahl und der Lücke zu verwenden.

$m: repeating-conic-gradient(#000 0% var(--p), transparent 0% $p);

Dann registrieren wir diese Variable.

CSS.registerProperty({
  name: '--p', 
  syntax: '<percentage>', 
  initialValue: '0%', 
  inherits: true
})

Und wir animieren sie im CSS mit einer Keyframe-animation.

.rays {
  /* same as before */
  animation: p .5s linear infinite alternate
}

@keyframes p { to { --p: #{$p} } }

Das Ergebnis ist in diesem Fall interessanter.

Animated gif. The stop position between the ray an the gap animates from 0 (when the ray is basically reduced to nothing) to the whole percentage $p allocated for a ray and the gap following it (which basically means we don't have a gap anymore) and then back to 0 again.
Animation alternierender Strahlen-Größen (Live-Demo, funktioniert nur in Blink-Browsern mit aktiviertem Flag Experimental Web Platform features).

Aber wir können es noch ein bisschen aufpeppen, indem wir das Ganze horizontal spiegeln, bevor jede Iteration, sodass es für die Rückwärts-Iterationen immer gespiegelt ist. Das bedeutet, nicht gespiegelt, wenn --p von 0% auf $p geht, und gespiegelt, wenn --p von $p zurück auf 0% geht.

Die Art und Weise, wie wir ein Element horizontal spiegeln, ist, indem wir ihm eine transform: scalex(-1) geben. Da wir möchten, dass dieser Spiegel effekt am Ende der ersten Iteration angewendet und dann am Ende der zweiten (rückwärtslaufenden) Iteration entfernt wird, wenden wir ihn ebenfalls in einer Keyframe-animation an – in einer mit einer steps()-Timing-Funktion und der doppelten animation-duration.

 $t: .5s;

.rays {
  /* same as before */
  animation: p $t linear infinite alternate, 
    s 2*$t steps(1) infinite;
}

@keyframes p { to { --p: #{$p} } }

@keyframes s { 50% { transform: scalex(-1); } }

Jetzt haben wir endlich ein Ergebnis, das wirklich ziemlich cool aussieht.

Animated gif. We have the same animation as before, plus a horizontal flip at the end of every iteration which creates the illusion of a circular sweep instead of just increasing and then decreasing rays, as the rays seems to now decrease from the start after they got to their maximum size incresing from the end.
Animation alternierender Strahlen-Größen mit horizontaler Spiegelung zwischen den Iterationen (Live-Demo, funktioniert nur in Blink-Browsern mit aktiviertem Flag Experimental Web Platform features).

CSS-ing Gradient Rays and Ripples

Um das Ergebnis mit Strahlen und Wellen zu erzielen, müssen wir dem mask einen zweiten Gradienten hinzufügen, diesmal einen repeating-radial-gradient().

SVG illustration. Connects the stop positions from the code to the actual corresponding points on the circle defining the repeating radial gradient.
Wie repeating-radial-gradient() funktioniert (live).
$nr: 20;
$p: 100%/$nr;
$stop-list: #000 0% .5*$p, transparent 0% $p;
$m: repeating-conic-gradient($stop-list), 
    repeating-radial-gradient(closest-side, $stop-list);

.rays-ripples {
  /* same as before */
  mask: $m;
}

Leider funktioniert die Verwendung mehrerer Stopp-Positionen nur in Blink-Browsern mit aktiviertem Flag Experimental Web Platform features. Und während das conic-gradient()-Polyfill dies für den repeating-conic-gradient()-Teil in Browsern abdeckt, die CSS-Maskierung auf HTML-Elementen unterstützen, aber keine konischen Gradienten nativ unterstützen (Firefox, Safari, Blink-Browser ohne aktiviertes Flag), behebt nichts das Problem für den repeating-radial-gradient()-Teil in diesen Browsern.

Das bedeutet, wir sind gezwungen, einige Wiederholungen in unserem Code zu haben.

$nr: 20;
$p: 100%/$nr;
$stop-list: #000, #000 .5*$p, transparent 0%, transparent $p;
$m: repeating-conic-gradient($stop-list), 
    repeating-radial-gradient(closest-side, $stop-list);

.rays-ripples {
  /* same as before */
  mask: $m;
}

Wir kommen offensichtlich näher, aber wir sind noch nicht ganz da.

Screenshot. We have the same radial slices with equal gaps in between, and over them, a layer of ripples - concentric rings with gaps equal to their width in between them. The whole thing has a top to bottom gradient (orange to purple) with transparent parts where the gaps of the two layers intersect.
Zwischenergebnis mit den beiden Maskierungsebenen (Live-Demo, keine Edge-Unterstützung).

Um das gewünschte Ergebnis zu erzielen, müssen wir die Eigenschaft mask-composite verwenden und sie auf exclude setzen.

mask-composite wird derzeit nur in Firefox 53+ unterstützt, obwohl WebKit-Browser eine sehr gute Unterstützung (seit Chrome 1.0 und Safari 4.0) für eine ähnliche nicht standardisierte Eigenschaft, -webkit-mask-composite, haben, die uns für einen Wert von xor das gleiche Ergebnis liefert. Edge soll folgen, wenn es die Maskierung von HTML-Elementen unterstützt. Beachten Sie jedoch, dass Edge sowohl mask-composite als auch -webkit-mask-composite mit den Standardwerten unterstützen wird, obwohl jeder, der -webkit-mask-composite zur Zielgruppenansprache von WebKit-Browsern verwendet, es wahrscheinlich mit den nicht standardmäßigen Werten verwenden wird, da es sonst nicht einmal in WebKit-Browsern funktioniert.

$lyr1: repeating-conic-gradient($stop-list); 
$lyr0: repeating-radial-gradient(closest-side, $stop-list);

.xor {
  /* same as before */
  -webkit-mask: $lyr1, $lyr0;
  -webkit-mask-composite: xor;
          mask: $lyr1 exclude, $lyr0
}

Beachten Sie, dass das nicht standardmäßige -webkit-mask-composite nicht innerhalb des -webkit-mask-Kurzbefehls verwendet werden kann, so wie wir mask-composite innerhalb des mask-Kurzbefehls für Firefox verwenden.

Screenshot. We have the same result as before, except now we have performed a XOR operation between the two layers (rays and ripples).
XOR-Strahlen und Wellen (Live-Demo, Firefox 53+ mit standardmäßigem mask-composite und Chrome 1.0+/Safari 4.0+ mit nicht standardmäßigem -webkit-mask-composite).

Wenn Sie denken, dass die Strahlen und die Lücken zwischen den Strahlen in Browsern, die conic-gradient() nicht nativ unterstützen, nicht gleich sind, haben Sie Recht. Dies liegt an einem Problem des Polyfills.

Hinzufügen von Animationen

Da der standardmäßige mask-composite derzeit nur in Firefox funktioniert und Firefox conic-gradient() noch nicht nativ unterstützt, können wir keine CSS-Variablen in repeating-conic-gradient() einfügen (da Firefox für diese immer noch auf das Polyfill zurückfällt und das Polyfill keine Verwendung von CSS-Variablen unterstützt). Aber wir können sie in repeating-radial-gradient() einfügen und selbst wenn wir sie nicht mit CSS-Keyframe-Animationen animieren können, können wir es mit JavaScript tun!

Da wir jetzt CSS-Variablen in repeating-radial-gradient(), aber nicht in repeating-conic-gradient() verwenden (da wir eine bessere Browserunterstützung wünschen und Firefox keine konischen Gradienten nativ unterstützt, sodass es auf das Polyfill zurückfällt, das keine CSS-Variablen verwendet), können wir nicht mehr die gleiche $stop-list für beide Gradientenebenen unserer mask verwenden.

Aber wenn wir unsere mask sowieso ohne eine gemeinsame $stop-list neu schreiben müssen, können wir diese Gelegenheit nutzen, um unterschiedliche Stopp-Positionen für die beiden Gradienten zu verwenden.

// for conic gradient
$nc: 20;
$pc: 100%/$nc;
// for radial gradient
$nr: 10;
$pr: 100%/$nr;

Die CSS-Variable, die wir animieren, ist eine Alpha-Variable --a, genau wie bei der ersten Animation im Fall der Strahlen. Wir führen auch die Variablen --c0 und --c1 ein, da wir hier nicht mehrere Positionen pro Stopp haben können und Wiederholungen so weit wie möglich vermeiden wollen.

$lyr1: repeating-conic-gradient(#000 0% .5*$pc, transparent 0% $pc);
$lyr0: repeating-radial-gradient(closest-side, 
          var(--c0), var(--c0) .5*$pr, 
          var(--c1) 0, var(--c1) $pr);

body {
  --a: 0;
  /* layout, backgrounds and other irrelevant stuff */
}

.xor {
  /* same as before */
  --c0: #{rgba(#000, var(--a))};
  --c1: #{rgba(#000, calc(1 - var(--a)))};
  -webkit-mask: $lyr1, $lyr0;
  -webkit-mask-composite: xor;
          mask: $lyr1 exclude, $lyr0
}

Die Alpha-Variable --a ist diejenige, die wir mit ein wenig Vanilla-JavaScript hin und her animieren (von 0 auf 1 und dann zurück auf 0). Wir beginnen damit, eine Gesamtzahl von Frames NF festzulegen, über die die Animation läuft, einen aktuellen Frame-Index f und eine aktuelle Animationsrichtung dir.

const NF = 50;

let f = 0, dir = 1;

Innerhalb einer update()-Funktion aktualisieren wir den aktuellen Frame-Index f und weisen dann den aktuellen Fortschrittswert (f/NF) der aktuellen Alpha --a zu. Wenn f entweder 0 oder NF erreicht hat, ändern wir die Richtung. Dann wird die update()-Funktion bei der nächsten Aktualisierung erneut aufgerufen.

(function update() {
  f += dir;
  
  document.body.style.setProperty('--a', (f/NF).toFixed(2));
	  
  if(!(f%NF)) dir *= -1;
  
  requestAnimationFrame(update)
})();

Und das ist alles für das JavaScript! Wir haben jetzt ein animiertes Ergebnis.

Animated gif. We animate the alpha of the gradient stops, such that the ripples go from fully opaque to fully transparent, effectively becoming gaps, while the opposite happens for the initial gaps, they go from fully transparent to fully opaque, thus becoming ripples. At any moment, the alpha of either of them is  1 minus the alpha of the other, so they complement each other. In this case, the animation is linear, the alpha changing at the same rate from start to finish.
Alpha-Animation der Wellen, linear (Live-Demo, Firefox 53+ mit standardmäßigem mask-composite und Chrome 1.0+/Safari 4.0+ mit nicht standardmäßigem -webkit-mask-composite).

Dies ist eine lineare Animation, bei der der Alpha-Wert --a auf den Fortschritt f/NF gesetzt wird. Aber wir können die Timing-Funktion zu etwas anderem ändern, wie in einem früheren Artikel von mir über das Emulieren von CSS-Timing-Funktionen mit JavaScript erklärt.

Wenn wir zum Beispiel eine Timing-Funktion vom Typ ease-in wollen, setzen wir den Alpha-Wert auf easeIn(f/NF) anstelle von nur f/NF, wobei easeIn() Folgendes ist:

function easeIn(k, e = 1.675) {
  return Math.pow(k, e)
}

Das Ergebnis bei Verwendung einer ease-in-Timing-Funktion ist in diesem Pen zu sehen (Firefox 53+ mit standardmäßigem mask-composite und Chrome 1.0+/Safari 4.0+ mit nicht standardmäßigem -webkit-mask-composite). Wenn Sie daran interessiert sind, wie wir diese Funktion erhalten haben, wird sie im zuvor verlinkten Artikel über Timing-Funktionen detailliert erklärt.

Der exakt gleiche Ansatz funktioniert für easeOut() oder easeInOut().

function easeOut(k, e = 1.675) {
  return 1 - Math.pow(1 - k, e)
};

function easeInOut(k) {
  return .5*(Math.sin((k - .5)*Math.PI) + 1)
}

Da wir sowieso JavaScript verwenden, können wir das Ganze interaktiv gestalten, sodass die Animation beispielsweise nur bei Klick/Tippen stattfindet.

Um dies zu tun, fügen wir eine Request-ID-Variable (rID) hinzu, die anfangs null ist, aber dann den von requestAnimationFrame() zurückgegebenen Wert in der update()-Funktion annimmt. Dies ermöglicht es uns, die Animation mit einer stopAni()-Funktion zu stoppen, wann immer wir wollen.

 /* same as before */

let rID = null;

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

function update() {
  /* same as before */
  
  if(!(f%NF)) {
    stopAni();
    return
  }
  
  rID = requestAnimationFrame(update)
};

Beim Klicken stoppen wir jede laufende Animation, kehren die Animationsrichtung dir um und rufen die update()-Funktion auf.

addEventListener('click', e => {
  if(rID) stopAni();
  dir *= -1;
  update()
}, false);

Da wir mit dem aktuellen Frame-Index f bei 0 beginnen, möchten wir bei jedem Klick in positiver Richtung, also in Richtung NF, gehen. Und da wir die Richtung bei jedem Klick umkehren, muss der anfängliche Wert für die Richtung jetzt -1 sein, damit er beim ersten Klick zu +1 umgekehrt wird.

Das Ergebnis all dessen ist in diesem interaktiven Pen zu sehen (funktioniert nur in Firefox 53+ mit standardmäßigem mask-composite und Chrome 1.0+/Safari 4.0+ mit nicht standardmäßigem -webkit-mask-composite).

Wir könnten auch eine andere Alpha-Variable für jeden Stopp verwenden, genau wie wir es im Fall der Strahlen getan haben.

$lyr1: repeating-conic-gradient(#000 0% .5*$pc, transparent 0% $pc);
$lyr0: repeating-radial-gradient(closest-side, 
           rgba(#000, var(--a0)), rgba(#000, var(--a1)) .5*$pr, 
           rgba(#000, var(--a2)) 0, rgba(#000, var(--a3)) $pr);

Im JavaScript haben wir die Timing-Funktionen ease-in und ease-out.

const TFN = {
  '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)
  }
};

In der update()-Funktion besteht der einzige Unterschied zur ersten animierten Demo darin, dass wir nicht nur eine CSS-Variable ändern – wir haben jetzt vier, um die wir uns kümmern müssen: --a0, --a1, --a2, --a3. Wir tun dies innerhalb einer Schleife, wobei wir die ease-in-Funktion für die mit geraden Indizes und die ease-out-Funktion für die anderen verwenden. Für die ersten beiden wird der Fortschritt durch f/NF gegeben, während für die letzten beiden der Fortschritt durch 1 - f/NF gegeben ist. Alles in eine Formel gepackt ergibt das:

(function update() {
  f += dir;
  
  for(var i = 0; i < 4; i++) {
    let j = ~~(i/2);
		
    document.body.style.setProperty(
      `--a${i}`, 
      TFN[i%2 ? 'ease-out' : 'ease-in'](j + Math.pow(-1, j)*f/NF).toFixed(2)
    )
  }
	  
  if(!(f%NF)) dir *= -1;
  
  requestAnimationFrame(update)
})();

Das Ergebnis ist unten zu sehen.

Animated gif. This time, the alpha of each and every stop (start and end of ripple, start and end of gap) is animated independently via its own CSS variable. The alphas at the start and end of the ripple both go from 1 to 0, but using different timing functions. The alphas at the start and end of the gap both go from 0 to 1, but, again, using different timing functions.
Animationen mehrerer Wellen-Alphas (Live-Demo, funktioniert nur in Firefox 53+ mit standardmäßigem mask-composite und Chrome 1.0+/Safari 4.0+ mit nicht standardmäßigem -webkit-mask-composite).

Genau wie bei konischen Gradienten können wir auch die Stopp-Position zwischen dem undurchsichtigen und dem transparenten Teil des maskierenden radialen Gradienten animieren. Dazu verwenden wir eine CSS-Variable --p für den Fortschritt dieser Stopp-Position.

$lyr1: repeating-conic-gradient(#000 0% .5*$pc, transparent 0% $pc); 
$lyr0: repeating-radial-gradient(closest-side, 
           #000, #000 calc(var(--p)*#{$pr}), 
           transparent 0, transparent $pr);

Das JavaScript ist fast identisch mit dem der ersten Alpha-Animation, nur dass wir keine Alpha-Variable --a aktualisieren, sondern eine Stopp-Fortschrittsvariable --p, und wir verwenden eine Funktion vom Typ ease-in-out.

/* same as before */

function easeInOut(k) {
  return .5*(Math.sin((k - .5)*Math.PI) + 1)
};

(function update() {
  f += dir;
  
  document.body.style.setProperty('--p', easeInOut(f/NF).toFixed(2));
	  
  /* same as before */
})();
Animated gif. The stop position between the ripple an the gap animates from 0 (when the ripple is basically reduced to nothing) to the whole percentage $pr allocated for a ripple and the gap following it (which basically means we don't have a gap anymore) and then back to 0 again.
Animation alternierender Wellengrößen (Live-Demo, funktioniert nur in Firefox 53+ mit standardmäßigem mask-composite und Chrome 1.0+/Safari 4.0+ mit nicht standardmäßigem -webkit-mask-composite).

Wir können den Effekt interessanter gestalten, indem wir einen transparent-Streifen vor dem undurchsichtigen Streifen hinzufügen und auch den Fortschritt der Stopp-Position --p0 animieren, wo wir von diesem transparent-Streifen zu dem undurchsichtigen wechseln.

$lyr1: repeating-conic-gradient(#000 0% .5*$pc, transparent 0% $pc); 
$lyr0: repeating-radial-gradient(closest-side, 
           transparent, transparent calc(var(--p0)*#{$pr}), 
           #000, #000 calc(var(--p1)*#{$pr}), 
           transparent 0, transparent $pr);

Im JavaScript müssen wir jetzt zwei CSS-Variablen animieren: --p0 und --p1. Wir verwenden eine ease-in-Timing-Funktion für die erste und eine ease-out für die zweite. Wir kehren auch die Animationsrichtung nicht mehr um.

const NF = 120, 
      TFN = {
        '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)
        }
      };

let f = 0;

(function update() {
  f = (f + 1)%NF;
	
  for(var i = 0; i < 2; i++)
    document.body.style.setProperty(`--p${i}`, TFN[i ? 'ease-out' : 'ease-in'](f/NF);
  
  requestAnimationFrame(update)
})();

Dies ergibt ein ziemlich interessantes Ergebnis.

Animated gif. We now have one extra transparent circular strip before the opaque and transparent ones we previously had. Initially, both the start and end stop positions of this first strip and the following opaque one are 0, so they're both reduced to nothing and the whole space is occupied by the last transparent strip. The end stop positions of both strips then animate from 0 to the whole percentage $pr allocated for one repetition of our radial gradient, but with different timing functions. The end stop position of the first opaque strip animates slowly at first and faster towards the end (ease-in), while the end stop position of the opaque strip animates faster at first and slower towards the end (ease-out). This makes the opaque strip in the middle grow from nothing at first as its end stop position increases faster than that of the first transparent strip (which determines the start stop position of the opaque strip), then shrink back to nothing as its end stop position ends up being equal to $pr, just like the end stop position of the first transparent strip. The whole cycle then repeats itself.
Animation doppelter Wellengrößen (Live-Demo, funktioniert nur in Firefox 53+ mit standardmäßigem mask-composite und Chrome 1.0+/Safari 4.0+ mit nicht standardmäßigem -webkit-mask-composite).