Hiding Text - Teil 3

Hiding Text - Teil 3

Tauchen Sie ein in die faszinierende Welt der Animation mit GSAP (GreenSock Animation Platform). Erleben Sie die Magie fließender Bewegungen, atemberaubender Effekte und dynamischer Interaktionen. Mit GSAP können Sie Ihre Website zum Leben erwecken und Ihren Benutzern ein unvergessliches Erlebnis bieten. Entdecken Sie die grenzenlosen Möglichkeiten von GSAP und lassen Sie Ihrer Kreativität freien Lauf!

EINLEITUNG

Den Onepager mit GSAP erweitern

Im dritten Teil erweitern wir unseren Onepager um zusätzliche Animationen und Interaktionen. Nachdem die grundlegende Seitenstruktur, die CSS-Effekte und die ersten JavaScript-Funktionen bereits stehen, kommt nun GSAP (GreenSock Animation Platform) zum Einsatz.

Mit GSAP können wir Animationen präzise steuern und sie beispielsweise direkt mit der Scrollposition oder der Bewegung des Mauszeigers verbinden. Zusätzlich verwenden wir die Plugins ScrollTrigger und ScrollToPlugin.

Damit erstellen wir im weiteren Verlauf unter anderem Texteffekte beim Scrollen, magnetische Elemente und einen kreisförmigen Effekt, der dem Mauszeiger folgt. Außerdem machen wir innerhalb dieses Kreises zusätzliche Inhalte sichtbar und passen die Bedienung später für mobile Geräte an.

Zunächst binden wir GSAP und die benötigten Plugins in unsere index.html ein:
// index.html

...

<script
  src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"
  integrity="sha512-7eHRwcbYkK4d9g/6tD/mhkf++eoTHwpNM9woBxtPUBWm67zeAfFC+HrdoE2GanKeocly/VxeLvIqwvCdk7qScg=="
  crossorigin="anonymous"
  referrerpolicy="no-referrer"
></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.1/ScrollTrigger.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.1/ScrollToPlugin.min.js"></script>

...

GSAP selbst bildet die Grundlage für unsere Animationen. ScrollTrigger ermöglicht es uns, Animationen abhängig von der Scrollposition auszuführen. ScrollToPlugin erweitert GSAP um Funktionen zum animierten Scrollen zu bestimmten Positionen innerhalb der Seite.

Damit ist die Grundlage geschaffen. Als ersten Effekt lassen wir im nächsten Schritt unsere Texte beim Scrollen sichtbar werden.

TEXTEFFEKT

Text beim Scrollen sichtbar machen

Als ersten GSAP-Effekt animieren wir unsere Texte abhängig von der Scrollposition. Der Text ist zunächst nur schwach sichtbar und wird beim Scrollen nach und nach vollständig hervorgehoben.

Die dafür benötigten Farben haben wir bereits als CSS-Variablen in :root definiert. Entscheidend ist hier --color-text-transparent:
// assets/styles.css

:root {
  --bg: #272c3b;
  --bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto,
    "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif,
    "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
  --color-body: #b7ab98;
  --color-text: rgb(183, 171, 152, 1);
  --color-text-transparent: rgb(183, 171, 152, 0.2);
  --color-text-navi: rgb(183, 171, 152, 0.5);
}

Mit einem Alpha-Wert von 0.2 ist die Farbe nur zu 20 Prozent deckend. Dadurch können wir den Text zunächst zurückhaltend darstellen und ihn anschließend über die bereits vorhandene Hintergrunddarstellung hervorheben.

Für die eigentliche Scrollanimation ergänzen wir unsere assets/scripts.js:
// assets/scripts.js

/* -------------------------------------------------------------------------- */
/*                                 text scroll                                */
/* -------------------------------------------------------------------------- */

gsap.registerPlugin(ScrollTrigger);

const textElements = gsap.utils.toArray(".text");

textElements.forEach((text) => {
  gsap.to(text, {
    backgroundSize: "100%",
    ease: "none",
    scrollTrigger: {
      trigger: text,
      start: "center 70%",
      end: "center 30%",
      scrub: true,
    },
  });
});

Zunächst registrieren wir mit gsap.registerPlugin(ScrollTrigger) das zuvor eingebundene ScrollTrigger-Plugin.
Anschließend sammelt gsap.utils.toArray(".text") alle Elemente mit der Klasse .text. Für jedes dieser Elemente erzeugen wir eine eigene GSAP-Animation.

Dabei wird backgroundSize während des Scrollens bis auf 100% vergrößert. Die eigentliche Textdarstellung haben wir bereits im ersten Teil über background-clip: text vorbereitet.

ScrollTrigger legt fest, wann die Animation beginnt und endet. Mit
start: "center 70%",
end: "center 30%",

beginnt die Animation, wenn sich die Mitte des jeweiligen Textelements auf Höhe von 70 Prozent des Viewports befindet. Sie endet, wenn die Mitte des Elements die 30-Prozent-Position erreicht.

Durch
scrub: true

ist die Animation direkt an die Scrollbewegung gekoppelt. Scrollen wir vor oder zurück, bewegt sich auch die Animation entsprechend mit.


31.mp4 17.8 MB


MAGNETISCHER EFFEKT

Elemente auf die Mausbewegung reagieren lassen

Im nächsten Schritt erstellen wir einen magnetischen Effekt für ausgewählte Elemente. Bewegen wir den Mauszeiger über ein solches Element, verschiebt es sich leicht in Richtung des Mauszeigers. Verlässt die Maus den Bereich wieder, bewegt sich das Element sanft zurück an seine ursprüngliche Position.

Welche Elemente auf diesen Effekt reagieren sollen, bestimmen wir über die bereits verwendete Klasse .magnetic. Unser Logo aus dem ersten Teil besitzt diese Klasse beispielsweise schon:
<div class="logo magnetic">
  <img src="assets/images/logo-white.webp" alt="b4um" />
</div>

Die eigentliche Bewegung ergänzen wir in unserer assets/scripts.js:
/* -------------------------------------------------------------------------- */
/*                               magnetic cursor                              */
/* -------------------------------------------------------------------------- */

const magnets = document.querySelectorAll(".magnetic");
const strength = 50;

magnets.forEach((magnet) => {
  magnet.addEventListener("mousemove", moveMagnet);

  magnet.addEventListener("mouseout", function (event) {
    gsap.to(event.currentTarget, {
      duration: 1,
      x: 0,
      y: 0,
      ease: "power4.out",
    });
  });
});

function moveMagnet(event) {
  const magnetButton = event.currentTarget;
  const bounding = magnetButton.getBoundingClientRect();

  gsap.to(magnetButton, {
    duration: 1,
    x:
      ((event.clientX - bounding.left) / magnetButton.offsetWidth - 0.5) *
      strength,
    y:
      ((event.clientY - bounding.top) / magnetButton.offsetHeight - 0.5) *
      strength,
    ease: "power4.out",
  });
}

Zunächst wählen wir mit document.querySelectorAll(".magnetic") alle Elemente aus, die auf die Mausbewegung reagieren sollen. Über strength bestimmen wir anschließend die Stärke der Bewegung.

Für jedes dieser Elemente registrieren wir zwei Ereignisse. mousemove wird ausgeführt, solange sich der Mauszeiger über dem Element bewegt. Dabei ruft der Event-Listener die Funktion moveMagnet() auf.

Mit getBoundingClientRect() ermitteln wir die aktuelle Position und Größe des Elements. Anschließend berechnen wir aus der Position des Mauszeigers, wie weit sich das Element horizontal und vertikal bewegen soll.

Befindet sich der Mauszeiger beispielsweise rechts von der Mitte, erhält das Element einen positiven X-Wert. Befindet er sich links davon, wird der Wert negativ. Dasselbe Prinzip verwenden wir für die vertikale Bewegung.

Die Bewegung selbst wird mit gsap.to() animiert. Über duration: 1 legen wir eine Animationsdauer von einer Sekunde fest. ease: "power4.out" sorgt dafür, dass die Bewegung zum Ende hin weich abbremst.

Sobald der Mauszeiger das Element verlässt, animieren wir x und y wieder auf 0. Dadurch bewegt sich das Element sanft zurück an seine ursprüngliche Position.


32.mp4 3.48 MB


KREISMASKE

Einen Kreis für die versteckten Inhalte vorbereiten

Als Nächstes erstellen wir die Grundlage für einen weiteren visuellen Effekt. Ein zusätzlicher Inhaltsbereich wird zunächst ausgeblendet und später 
über eine kreisförmige Maske sichtbar gemacht. Die Position und Größe dieses Kreises können wir anschließend mit GSAP steuern.

Zunächst ergänzen wir dafür die benötigten Styles:
// assets/styles.css

.hidden-content {
  --x: 0px;
  --y: 0px;
  --size: 20px;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
  background-color: #eb5939;
  color: var(--bg);
  --mask: radial-gradient(
    circle at var(--x) var(--y),
    black var(--size),
    transparent 0
  );
  -webkit-mask-image: var(--mask);
  mask-image: var(--mask);
  pointer-events: none;
  visibility: hidden;
  display: none;
}

.hidden-content.active {
  display: block;
}

#hiddenContents {
  -webkit-transition: all 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
  transition: all 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}

Die drei CSS-Variablen --x, --y und --size bilden die Grundlage der Kreisbewegung. --x und --y bestimmen die Position des Kreises, während --size seinen Radius festlegt.

Die eigentliche Maske entsteht über einen radialen Verlauf:
--mask: radial-gradient(
  circle at var(--x) var(--y),
  black var(--size),
  transparent 0
);

Der schwarze Bereich der Maske macht den entsprechenden Teil des Elements sichtbar, während der transparente Bereich ausgeblendet bleibt. Über mask-image beziehungsweise -webkit-mask-image wenden wir diese Maske auf .hidden-content an.

Mit pointer-events: none stellen wir sicher, dass der zusätzliche Inhaltsbereich keine Mausereignisse abfängt. Links und andere interaktive Elemente der eigentlichen Seite bleiben dadurch weiterhin erreichbar.

Zu Beginn ist .hidden-content ausgeblendet. Sobald später die Klasse .active hinzugefügt wird, können wir den Bereich anzeigen und die Kreisgröße sowie seine Position mit GSAP verändern.

Nun ergänzen wir innerhalb unseres <main>-Bereichs hinter .content und vor dem <aside> zunächst den Container für den versteckten Inhalt:
// index.html

...

<div class="hidden-content" id="hiddenContents"></div>

...
Dieser Container bleibt zunächst leer. Im weiteren Verlauf füllen wir ihn mit einer zweiten Inhaltsebene, die durch unsere Kreis-Maske sichtbar wird.

KREIS ANIMIEREN

Die Maske der Maus folgen lassen

Nachdem wir die kreisförmige Maske mit CSS vorbereitet haben, steuern wir sie nun mit GSAP. Der Kreis soll sich an der Position des Mauszeigers befinden und größer werden, sobald wir mit der Maus über einen unserer Inhaltsbereiche fahren.

Dazu ergänzen wir unsere assets/scripts.js:
// assets/scripts.js

/* -------------------------------------------------------------------------- */
/*                                    Kreis                                   */
/* -------------------------------------------------------------------------- */

gsap.set(".hidden-content", { xPercent: 0, yPercent: 0 });

const speed = 0.08;

const hiddenContent = document.getElementById("hiddenContents");
const content = document.querySelector(".content");
const mouse = { x: 0, y: 0 };

const xSet = gsap.quickSetter(hiddenContent, "--x", "px");
const ySet = gsap.quickSetter(hiddenContent, "--y", "px");

let linkAnimated = false;

const xTo = gsap.quickTo(hiddenContent, "--x", {
  duration: 1,
  ease: "power4.out",
});

const yTo = gsap.quickTo(hiddenContent, "--y", {
  duration: 1,
  ease: "power4.out",
});

const tl = gsap.timeline({ paused: true });

if (window.innerWidth < 1249) {
  tl.to(hiddenContent, {
    "--size": 250,
    position: "absolute",
    display: "block",
    duration: 0.75,
    ease: "expo.out",
  });
} else {
  tl.to(hiddenContent, {
    "--size": 250,
    position: "absolute",
    duration: 0.75,
    ease: "expo.out",
  });
}

const hoveringContent = gsap.utils.toArray(".display", content);

hoveringContent.forEach((el) => {
  el.addEventListener("mouseenter", () => {
    tl.restart();
  });

  el.addEventListener("mouseleave", () => {
    tl.reverse();
  });
});

/* -------------------------------------------------------------------------- */
/*               Füge eine Maske bei der ersten Mausbewegung hinzu            */
/* -------------------------------------------------------------------------- */

window.addEventListener("mousemove", onFirstMove);

function onFirstMove(e) {
  window.removeEventListener("mousemove", onFirstMove);

  gsap.set(hiddenContent, {
    autoAlpha: 1,
    "--x": e.pageX,
    "--y": e.pageY,
  });

  window.addEventListener("mousemove", (e) => {
    if (!linkAnimated) {
      yTo(e.pageY);
      xTo(e.pageX);
    }
  });
}

/* -------------------------------------------------------------------------- */
/*           Klasse 'active' dynamisch hinzufügen und entfernen               */
/* -------------------------------------------------------------------------- */

const hiddenLinks = document.querySelectorAll(".content");

hiddenLinks.forEach((link) => {
  link.addEventListener("mouseenter", () => {
    const hiddenContent = link.nextElementSibling;
    hiddenContent.classList.add("active");
  });

  link.addEventListener("mouseleave", () => {
    const hiddenContent = link.nextElementSibling;
    hiddenContent.classList.remove("active");
  });
});

Zunächst holen wir uns Referenzen auf den versteckten Inhaltsbereich und den normalen .content-Bereich. Die CSS-Variablen --x und --y, die wir zuvor für unsere Maske angelegt haben, können anschließend direkt über GSAP verändert werden.

Mit gsap.quickSetter() erstellen wir Funktionen, mit denen sich diese Werte besonders effizient aktualisieren lassen:
const xSet = gsap.quickSetter(hiddenContent, "--x", "px");
const ySet = gsap.quickSetter(hiddenContent, "--y", "px");
Zusätzlich verwenden wir gsap.quickTo(). Dadurch bewegt sich die Maske nicht abrupt von einer Position zur nächsten, sondern folgt der Maus mit einer weichen Animation:
ease: "power4.out"

Anschließend erstellen wir eine pausierte GSAP-Timeline. Sie verändert --size auf 250 und vergrößert damit den sichtbaren Bereich unserer radialen Maske.

Auf kleineren Displays wird zusätzlich display: "block" gesetzt. Die spezielle Bedienung für mobile Geräte ergänzen wir später noch separat.

Mit
gsap.utils.toArray(".display", content);
sammeln wir alle .display-Elemente innerhalb unseres normalen Inhaltsbereichs. Betritt der Mauszeiger einen solchen Bereich, starten wir die Timeline mit tl.restart(). Verlässt er ihn wieder, läuft sie mit tl.reverse() rückwärts und der Kreis wird wieder kleiner.

Beim ersten mousemove positionieren wir die Maske an der aktuellen Mausposition und machen den versteckten Bereich über autoAlpha sichtbar. Anschließend aktualisieren xTo() und yTo() die Position bei weiteren Mausbewegungen.

Zum Schluss reagieren wir auf das Betreten und Verlassen des normalen .content-Bereichs. Dabei wird die Klasse .active am direkt folgenden .hidden-content ein- beziehungsweise ausgeschaltet. Diese Klasse haben wir im vorherigen CSS-Abschnitt bereits vorbereitet.

Damit besitzen wir nun einen kreisförmigen Bereich, dessen Position von der Maus gesteuert wird und dessen Größe sich beim Überfahren unserer Inhalte animiert.


31.webp 119 KB

KREISBEWEGUNG

Den Kreis beim Scrollen und Bewegen der Maus aktualisieren

Unsere Maske reagiert bereits auf die Mausbewegung. Beim Scrollen der Seite müssen wir ihre Position jedoch ebenfalls aktualisieren, damit der Kreis weiterhin an der richtigen Stelle zum Mauszeiger dargestellt wird.

Dafür ergänzen wir den folgenden Code in unserer assets/scripts.js:
// assets/scripts.js

/* -------------------------------------------------------------------------- */
/*                        Kreis beim Scrollen aktualisieren                    */
/* -------------------------------------------------------------------------- */

window.addEventListener("scroll", () => {
  const mouseX = mouse.x + window.scrollX;
  const mouseY = mouse.y + window.scrollY;

  if (!linkAnimated) {
    ySet(mouseY);
    xSet(mouseX);
  }
});

window.addEventListener("mousemove", (e) => {
  const dt = 1.0 - Math.pow(1.0 - speed, gsap.ticker.deltaRatio());

  mouse.x = e.clientX;
  mouse.y = e.clientY;

  if (!linkAnimated) {
    ySet(mouse.y + window.scrollY * dt);
    xSet(mouse.x + window.scrollX * dt);
  }
});

Bei jeder Mausbewegung speichern wir zunächst die aktuelle Position des Mauszeigers:
mouse.x = e.clientX;
mouse.y = e.clientY;

clientX und clientY liefern die Position innerhalb des aktuell sichtbaren Browserfensters. Da sich unsere Seite jedoch auch scrollen lässt, berücksichtigen wir zusätzlich window.scrollX und window.scrollY.

Beim Scrollen berechnen wir deshalb die Position des Mauszeigers innerhalb des gesamten Dokuments neu:
const mouseX = mouse.x + window.scrollX;
const mouseY = mouse.y + window.scrollY;

Anschließend setzen xSet() und ySet() die CSS-Variablen --x und --y unserer Maske auf die neuen Werte.

Für die Mausbewegung berechnen wir zusätzlich den Wert dt:
const dt = 1.0 - Math.pow(1.0 - speed, gsap.ticker.deltaRatio());

gsap.ticker.deltaRatio() berücksichtigt die tatsächlich vergangene Zeit zwischen den einzelnen Aktualisierungen. Zusammen mit unserem zuvor definierten speed-Wert erhalten wir damit einen Wert, mit dem die Scrollbewegung angepasst wird.

Die Abfrage
if (!linkAnimated)

verhindert außerdem, dass die Position der Maske durch diese Event-Listener verändert wird, während ein anderer Effekt die Kontrolle über den Kreis übernommen hat. Diese Funktion benötigen wir später noch einmal.

Damit kann die Position der Kreis-Maske sowohl auf Mausbewegungen als auch auf das Scrollen der Seite reagieren.


33.mp4 12.8 MB



VERSTECKTER INHALT

Text innerhalb des Kreises sichtbar machen

Bisher ist unser .hidden-content noch leer. Die Kreis-Maske funktioniert bereits, innerhalb des sichtbaren Bereichs gibt es aber noch keinen eigentlichen Inhalt.

Jetzt ergänzen wir eine zweite Variante unseres Hero-Bereichs innerhalb von #hiddenContents. Dieser Inhalt wird nur dort sichtbar, wo unsere kreisförmige Maske die versteckte Ebene freigibt.

Dazu ersetzen wir den bisher leeren Container:
<div class="hidden-content" id="hiddenContents"></div>

durch:
// index.html

<div class="hidden-content" id="hiddenContents">
  <!-- -------------------------------- hero --------------------------------- -->
  <div class="row justify-content-center align-items-center min-vh-100">
    <div class="display">
      <p class="about_content_label text-align-center">b4um</p>
      <div class="text-center text-black">Hiding</div>
      <div class="text-center text-black"><strong>bad</strong></div>
      <div class="text-center text-black"><strong>shit</strong></div>
      <div class="text-center text-black">since</div>
      <div class="text-center text-black">2023</div>
    </div>
  </div>
  <!-- ------------------------------ hero ende ------------------------------ -->
</div>

Die Struktur entspricht unserem bereits vorhandenen Hero-Bereich. Der entscheidende Unterschied ist, dass sich diese Variante innerhalb von .hidden-content befindet.

Dadurch liegen zwei Inhaltsebenen übereinander: Die normale Seite bleibt sichtbar und die versteckte Ebene wird nur innerhalb unserer beweglichen Kreis-Maske freigegeben.

Für die hervorgehobenen Wörter ergänzen wir außerdem noch eine kleine CSS-Regel:
// assets/styles.css

.hidden-content strong {
  color: #0d0d0d;
  font-weight: 900;
}

Die Regel gilt ausschließlich für <strong>-Elemente innerhalb von .hidden-content. Dadurch können wir einzelne Wörter auf der versteckten Ebene besonders kräftig und dunkel darstellen, ohne die <strong>-Elemente der übrigen Seite zu verändern.

Bewegen wir nun den Kreis über den Hero-Bereich, erscheint innerhalb der Maske der alternative Inhalt der versteckten Ebene.


33.mp4 12.8 MB



ABOUT ME

Die versteckte Ebene für den nächsten Bereich ergänzen

Nachdem wir den alternativen Hero-Bereich in unsere versteckte Ebene eingefügt haben, erweitern wir diese nun passend zum nächsten Abschnitt unserer normalen Seite.

Innerhalb von #hiddenContents fügen wir direkt nach dem versteckten Hero-Bereich den folgenden Abschnitt ein:
// index.html

<!-- ------------------------------ About me ------------------------------- -->
<div class="row justify-content-center align-items-center min-vh-100">
  <div class="display width-100">
    <p class="about_content_label">About me</p>
    <div class="display-hidden">
      Ein visueller Designer - mit Fähigkeiten
    </div>
    <div class="display-hidden">
      die noch nicht von KI ersetzt wurden - der nur gute Arbeit macht,
    </div>
    <div class="display-hidden">wenn das Gehalt ebenso gut ist.</div>
  </div>
</div>
<!-- ---------------------------- About me ende ---------------------------- -->

Die bereits vorhandenen Hilfsklassen row, justify-content-center, align-items-center und min-vh-100 sorgen dafür, dass sich auch dieser versteckte Bereich an der Struktur des entsprechenden sichtbaren Abschnitts orientiert.

Für die Texte verwenden wir die neue Klasse .display-hidden. Diese gestalten wir nun in unserer assets/styles.css:
// assets/styles.css

.display-hidden {
  font-size: 2rem;
  font-weight: 600;
  line-height: 100%;
  width: 100%;
  display: flex;
  flex-direction: row;
  align-items: start;
  justify-content: start;
  position: relative;
  max-width: 1248px;
  margin: auto;
  text-align: left;
  padding-left: 1rem;
  padding-right: 1rem;
}

@media (min-width: 1248px) {
  .display-hidden {
    font-size: 4vw;
    padding-left: 0rem;
    padding-right: 0rem;
  }
}

Auf kleineren Displays verwenden wir zunächst eine Schriftgröße von 2rem und seitliche Abstände von jeweils 1rem.

Ab einer Breite von 1248px wird die Schriftgröße mit 4vw abhängig von der Breite des Browserfensters berechnet. Gleichzeitig entfernen wir die zusätzlichen seitlichen Innenabstände.

Durch max-width: 1248px bleibt der Textbereich auf großen Bildschirmen begrenzt, während margin: auto ihn innerhalb der verfügbaren Breite zentriert.

Damit besitzt nun auch der About-me-Bereich einen eigenen Inhalt auf der versteckten Ebene, der innerhalb unserer beweglichen Kreis-Maske sichtbar werden kann.

WHAT I DO

Einen leeren Bereich in der versteckten Ebene anlegen

Auch der nächste Bereich unserer normalen Seite benötigt ein entsprechendes Gegenstück innerhalb von #hiddenContents.

Für What I Do benötigen wir allerdings keinen zusätzlichen Text. Stattdessen legen wir lediglich einen leeren Bereich mit derselben Höhe wie der sichtbare Abschnitt an.

Direkt nach unserem versteckten About-me-Bereich ergänzen wir:
// index.html

<!-- ----------------------------- what i do ------------------------------- -->
<div
  class="row justify-content-center align-items-center min-vh-100"
></div>
<!-- ----------------------------- ende what i do -------------------------- -->

Obwohl dieser Bereich keinen eigenen Inhalt enthält, ist er für den Aufbau unserer versteckten Ebene wichtig.

Die Klasse min-vh-100 sorgt dafür, dass der Bereich mindestens die Höhe des Browserfensters einnimmt. Dadurch bleibt die vertikale Struktur der versteckten Ebene an dieser Stelle erhalten.

Da wir innerhalb der Kreis-Maske für What I Do keinen alternativen Inhalt anzeigen möchten, bleibt das Element bewusst leer.
Auf diese Weise folgt der nächste versteckte Abschnitt wieder an der passenden Position.

EXPERIENCE

Den versteckten Inhalt für Experience ergänzen

Nach dem leeren Bereich für What I Do folgt der Abschnitt Experience. Auch für diesen Bereich erstellen wir wieder eine alternative Textebene, die innerhalb unserer Kreis-Maske sichtbar wird.

Direkt nach dem leeren What-I-Do-Bereich ergänzen wir innerhalb von #hiddenContents:
// index.html

<!-- ----------------------------- experience ------------------------------ -->
<div class="row justify-content-center align-items-center min-vh-100">
  <div class="display">
    <p class="about_content_label">Experience</p>
    <div class="display-hidden">
      Nur sieben Jahre lang habe ich aktiv
    </div>
    <div class="display-hidden">
      coole Sachen produziert. Die anderen Jahre
    </div>
    <div class="display-hidden">waren eher ein Herumprobieren und</div>
    <div class="display-hidden">Durchlaufen meiner Karriere.</div>
  </div>
</div>
<!-- --------------------------- experience ende --------------------------- -->

Wie schon beim About-me-Bereich verwenden wir für die einzelnen Textzeilen die Klasse .display-hidden.

Die dafür benötigten Styles haben wir bereits im vorherigen Abschnitt angelegt. Deshalb müssen wir für Experience kein zusätzliches CSS hinzufügen.

Durch
class="row justify-content-center align-items-center min-vh-100"
erhält auch dieser Abschnitt wieder die entsprechende Höhe und Ausrichtung innerhalb der versteckten Ebene.

Der eigentliche Text befindet sich in einem .display-Element. Diese Klasse ist außerdem für unsere zuvor erstellte GSAP-Timeline wichtig: Beim Überfahren der entsprechenden .display-Bereiche wird die Kreis-Maske vergrößert beziehungsweise wieder verkleinert.

Die einzelnen .display-hidden-Elemente übernehmen das bereits festgelegte responsive Schriftbild. Auf kleineren Displays verwenden sie eine feste Schriftgröße, während die Schrift auf größeren Bildschirmen proportional zur Breite des Browserfensters skaliert.

Damit besitzt nun auch der Experience-Bereich seine eigene versteckte Textebene.

HISTORY

Einen leeren Bereich für History anlegen

Nach Experience folgt auf unserer normalen Seite der Bereich History. Auch hier benötigen wir innerhalb der versteckten Ebene keinen zusätzlichen Text.

Damit die vertikale Struktur von #hiddenContents weiterhin mit der sichtbaren Seite übereinstimmt, legen wir wieder einen leeren Bereich mit mindestens der Höhe des Browserfensters an.

Direkt nach dem versteckten Experience-Bereich ergänzen wir:
// index.html

<!-- ------------------------------- history ------------------------------- -->
<div
  class="row justify-content-center align-items-center min-vh-100"
></div>
<!-- ---------------------------- history ende ----------------------------- -->

Wie bereits bei What I Do dient dieses leere Element als Platzhalter innerhalb unserer versteckten Ebene.

Die Klasse min-vh-100 sorgt dafür, dass der Abschnitt mindestens die Höhe des Browserfensters einnimmt. Dadurch bleibt die Reihenfolge und vertikale Aufteilung der versteckten Inhalte erhalten, obwohl wir für History keinen alternativen Text anzeigen.

Innerhalb der Kreis-Maske bleibt dieser Abschnitt daher inhaltlich leer. Erst beim darauffolgenden Bereich fügen wir wieder einen sichtbaren Inhalt zur versteckten Ebene hinzu.

CLIENTS

Den versteckten Inhalt für Clients ergänzen

Nach dem leeren History-Bereich folgt der Abschnitt Clients. Hier möchten wir innerhalb unserer Kreis-Maske wieder einen alternativen Text anzeigen.

Dazu ergänzen wir innerhalb von #hiddenContents direkt nach dem History-Bereich:
// index.html

<!-- ------------------------------- Clients ------------------------------- -->
<div class="row justify-content-center align-items-center min-vh-100">
  <div class="display">
    <p class="about_content_label">Clients</p>
    <div class="display-hidden">
      Ich war nur ein kleiner Teil eines großen
    </div>
    <div class="display-hidden">
      Teams. Sie haben wahrscheinlich keine
    </div>
    <div class="display-hidden">Ahnung, dass ich existiere.</div>
  </div>
</div>
<!-- ---------------------------- Clients ende ----------------------------- -->

Auch hier verwenden wir wieder .display als umschließendes Element und .display-hidden für die einzelnen Textzeilen.

Die Gestaltung von .display-hidden haben wir bereits beim About-me-Bereich festgelegt. Deshalb benötigen wir für diesen Abschnitt kein zusätzliches CSS.

Durch die Klasse .display reagiert der Bereich außerdem auf unsere zuvor eingerichtete GSAP-Timeline. Bewegt sich der Mauszeiger über den entsprechenden Inhaltsbereich, kann die Kreis-Maske vergrößert und der alternative Clients-Text innerhalb der versteckten Ebene sichtbar gemacht werden.

Die Kombination aus sichtbarer und versteckter Ebene bleibt damit auch in diesem Abschnitt erhalten.

CLIENTS-AUFZÄHLUNG

Einen leeren Bereich für die Kundenliste anlegen

Direkt nach unserem Clients-Bereich folgt auf der normalen Seite die Aufzählung der einzelnen Kunden. Für diesen Bereich möchten wir innerhalb der Kreis-Maske keinen zusätzlichen Inhalt anzeigen.

Damit die versteckte Ebene trotzdem dieselbe vertikale Struktur wie die normale Seite behält, ergänzen wir innerhalb von #hiddenContents einen weiteren leeren Bereich:
// index.html

<!-- ------------------------- clients aufzählung -------------------------- -->
<div
  class="row justify-content-center align-items-center min-vh-100"
></div>
<!-- --------------------------- aufzählung ende --------------------------- -->

Wie bei den zuvor angelegten leeren Bereichen für What I Do und History dient dieses Element als Platzhalter.

Mit min-vh-100 erhält der Bereich mindestens die Höhe des Browserfensters. Dadurch bleibt innerhalb von #hiddenContents der notwendige Platz für die Clients-Aufzählung erhalten, obwohl dort kein alternativer Text dargestellt wird.

Die Kreis-Maske kann diesen Abschnitt somit passieren, ohne dass auf der versteckten Ebene zusätzlicher Inhalt erscheint. Der darauffolgende versteckte Bereich befindet sich anschließend wieder an der vorgesehenen vertikalen Position.

MOTTO

Den letzten versteckten Inhaltsbereich ergänzen

Zum Abschluss unserer versteckten Ebene ergänzen wir noch den Bereich Motto. Anders als bei den zuvor angelegten leeren Platzhaltern enthält dieser Abschnitt wieder eigenen Text, der innerhalb unserer Kreis-Maske sichtbar wird.

Direkt nach der leeren Clients-Aufzählung fügen wir innerhalb von #hiddenContents folgenden HTML-Code ein:
// index.html

<!-- ------------------------------- Motto ------------------------------- -->
<div class="row justify-content-center align-items-center min-vh-100">
  <div class="display">
    <p class="about_content_label text-align-center">Mein Motto</p>
    <div class="text-center text-black">Nicht alle</div>
    <div class="text-center text-black">ehrlichen Designs</div>
    <div class="text-center text-black">sind gut</div>
    <p class="about_content_label text-align-center">b4um</p>
  </div>
</div>
<!-- ---------------------------- Motto ende ----------------------------- -->

Wie bereits beim versteckten Hero-Bereich verwenden wir hier die vorhandenen Klassen text-center und text-black, um den Text mittig auszurichten und auf der farbigen versteckten Ebene dunkel darzustellen.

Die beiden Absätze mit .about_content_label bilden den oberen und unteren Abschluss des Bereichs. Mit .text-align-center werden auch diese Beschriftungen zentriert.

Der gesamte Inhalt befindet sich wiederum innerhalb eines .display-Elements. Dadurch wird auch dieser Bereich von unserer bereits eingerichteten Kreis-Animation berücksichtigt.

Zusätzliches CSS benötigen wir an dieser Stelle nicht, da wir ausschließlich Klassen verwenden, die bereits in unserem Stylesheet vorhanden sind.

Damit haben wir die versteckte Ebene für die einzelnen Bereiche unseres Onepagers aufgebaut. Sie folgt der Struktur der normalen Seite und kann über unsere kreisförmige Maske Stück für Stück sichtbar gemacht werden.


34.mp4 43.4 MB


KREIS AUSBLENDEN

Die Kreis-Maske über bestimmten Elementen ausblenden

Unsere Kreis-Maske folgt inzwischen dem Mauszeiger und macht die alternative Inhaltsebene sichtbar. Bei bestimmten Elementen möchten wir diesen Effekt jedoch vorübergehend deaktivieren.

Dafür verwenden wir die bereits vorhandene Klasse .hidden-link. Sobald sich der Mauszeiger über einem solchen Element befindet, blenden wir die versteckte Ebene aus. Verlässt der Mauszeiger das Element wieder, wird sie erneut sichtbar.

Dazu ergänzen wir unsere assets/scripts.js:
// assets/scripts.js

/* -------------------------------------------------------------------------- */
/*                          Verschwinden des Kreises                           */
/* -------------------------------------------------------------------------- */

document.querySelectorAll(".hidden-link").forEach((link) => {
  link.addEventListener("mouseenter", (e) => {
    linkAnimated = true;

    gsap.set(hiddenContent, {
      autoAlpha: 0,
      "--x": e.pageX,
      "--y": e.pageY,
    });
  });

  link.addEventListener("mouseleave", (e) => {
    linkAnimated = false;

    gsap.set(hiddenContent, {
      xPercent: 0,
      yPercent: 0,
    });

    gsap.set(hiddenContent, {
      autoAlpha: 1,
      "--x": e.pageX,
      "--y": e.pageY,
    });
  });
});

Zunächst suchen wir mit
document.querySelectorAll(".hidden-link")

alle Elemente, bei denen die Kreis-Maske vorübergehend verschwinden soll.

Beim Betreten eines solchen Elements setzen wir:
linkAnimated = true;

Diese Variable haben wir bereits beim Erstellen unserer Kreissteuerung angelegt. Solange sie true ist, verhindern die dort eingebauten Abfragen, dass andere Maus- oder Scroll-Ereignisse gleichzeitig die Position der Maske verändern.

Anschließend setzen wir mit:
autoAlpha: 0

die versteckte Ebene unsichtbar. autoAlpha ist eine GSAP-Eigenschaft, die sowohl die Deckkraft als auch die Sichtbarkeit des Elements steuert.

Die aktuelle Mausposition speichern wir gleichzeitig wieder in den CSS-Variablen --x und --y.

Verlässt der Mauszeiger das .hidden-link-Element, setzen wir linkAnimated wieder auf false. Die normale Steuerung des Kreises kann damit wieder übernehmen.

Anschließend setzen wir die Transformation des versteckten Bereichs zurück und machen ihn mit
autoAlpha: 1

wieder sichtbar. Gleichzeitig wird die Kreis-Maske an der aktuellen Position des Mauszeigers eingeblendet.

Damit können wir einzelne Bereiche oder Links von unserem Kreis-Effekt ausnehmen, ohne die eigentliche Kreissteuerung zu deaktivieren.

KREISPOSITION KORRIGIEREN

Beim Wechsel von Scrollen zu Mausbewegung die Position übernehmen

Wenn wir durch die Seite scrollen und anschließend die Maus bewegen, soll die Kreis-Maske nicht plötzlich von einer falschen Position aus erscheinen. Deshalb passen wir die Funktion onFirstMove() an.

Die bisherige Funktion onFirstMove() wird dabei vollständig ersetzt. Wir fügen also keine zweite Funktion mit demselben Namen hinzu.
Ersetze:
function onFirstMove(e) {
  window.removeEventListener("mousemove", onFirstMove);

  gsap.set(hiddenContent, {
    autoAlpha: 1,
    "--x": e.pageX,
    "--y": e.pageY,
  });

  window.addEventListener("mousemove", (e) => {
    if (!linkAnimated) {
      yTo(e.pageY);
      xTo(e.pageX);
    }
  });
}

durch:
// assets/scripts.js

/* -------------------------------------------------------------------------- */
/*                      Wechsel von Scrollen auf Bewegen                      */
/* -------------------------------------------------------------------------- */

function onFirstMove(e) {
  window.removeEventListener("mousemove", onFirstMove);

  // Mausposition innerhalb des sichtbaren Browserfensters
  const mouseX = Math.min(Math.max(e.clientX, 0), window.innerWidth);
  const mouseY = Math.min(Math.max(e.clientY, 0), window.innerHeight);

  // Scrollposition hinzufügen, da die Maske mit Dokumentkoordinaten arbeitet
  const xPosition = mouseX + window.scrollX;
  const yPosition = mouseY + window.scrollY;

  gsap.set(hiddenContent, {
    autoAlpha: 1,
    "--x": xPosition,
    "--y": yPosition,
  });

  window.addEventListener("mousemove", moveCircle);
}

function moveCircle(e) {
  if (linkAnimated) {
    return;
  }

  const mouseX = e.clientX + window.scrollX;
  const mouseY = e.clientY + window.scrollY;

  gsap.set(hiddenContent, {
    "--x": mouseX,
    "--y": mouseY,
  });
}

Für die Position innerhalb des sichtbaren Browserfensters verwenden wir jetzt clientX und clientY. Diese Koordinaten beziehen sich auf den Viewport und können deshalb sinnvoll mit window.innerWidth und window.innerHeight begrenzt werden.

Anschließend addieren wir die aktuelle Scrollposition:
const xPosition = mouseX + window.scrollX;
const yPosition = mouseY + window.scrollY;

Damit erhalten wir die Position innerhalb des gesamten Dokuments, die wir für unsere Kreis-Maske benötigen.

Bei der ersten Mausbewegung setzen wir den Kreis direkt auf diese Position und machen die versteckte Ebene sichtbar:
gsap.set(hiddenContent, {
  autoAlpha: 1,
  "--x": xPosition,
  "--y": yPosition,
});

Für alle weiteren Mausbewegungen verwenden wir die separate Funktion moveCircle().

Auch dort kombinieren wir die Mausposition innerhalb des Browserfensters mit der aktuellen Scrollposition. Dadurch bleibt die Position der Maske auch dann korrekt, wenn die Seite zuvor gescrollt wurde.

Zusätzlich berücksichtigen wir weiterhin unsere Variable linkAnimated:
if (linkAnimated) {
  return;
}

Ist ein Bereich aktiv, bei dem wir den Kreis vorübergehend ausblenden, beendet die Funktion ihre Ausführung und verändert die Position der Maske nicht.

Damit kann der Wechsel zwischen Scrollen und Mausbewegung erfolgen, ohne dass die Kreis-Maske durch unterschiedliche Koordinatensysteme an eine falsche Position springt.


35.mp4 50.6 MB

Mobile Ansicht

Den Kreis-Effekt für Touch-Geräte vorbereiten

Unser bisheriger Kreis-Effekt wird mit Mausbewegungen gesteuert. Auf Smartphones und kleineren Tablets steht diese Art der Bedienung jedoch nicht zur Verfügung. Deshalb unterscheiden wir im nächsten Schritt zwischen der Desktop- und der mobilen Ansicht.

Dafür erstellen wir zunächst eine kleine Hilfsfunktion in unserer assets/scripts.js:
// assets/scripts.js

/* -------------------------------------------------------------------------- */
/*                                   mobile                                   */
/* -------------------------------------------------------------------------- */

function isMobileDevice() {
  return window.innerWidth < 1248;
}

Die Funktion prüft die aktuelle Breite des Browserfensters. Unterhalb von 1248px behandeln wir die Darstellung für unseren Effekt als mobile Ansicht.

Damit verhindern wir anschließend, dass der mousemove-Listener für die erstmalige Aktivierung des Kreises auf diesen kleineren Ansichten registriert wird.

Suche dafür die bisherige Zeile:
window.addEventListener("mousemove", onFirstMove);

und ersetze sie durch:
if (!isMobileDevice()) {
  window.addEventListener("mousemove", onFirstMove);
}

Auf einer Desktop-Ansicht ab 1248px funktioniert unser bisheriger Maus-Effekt damit weiterhin wie gewohnt.

Auf kleineren Ansichten wird onFirstMove() dagegen nicht über eine Mausbewegung gestartet. Für diese Geräte erstellen wir im nächsten Schritt eine eigene Touch-Steuerung.

Damit verwenden Desktop und mobile Ansicht jeweils die Eingabemethode, die für das entsprechende Gerät beziehungsweise die entsprechende Darstellung vorgesehen ist.

TOUCH-BUTTON

Eine Steuerung für die mobile Ansicht hinzufügen

Da die Kreis-Maske auf dem Desktop durch den Mauszeiger gesteuert wird, benötigen wir für kleinere Displays eine alternative Bedienmöglichkeit.

Dafür erstellen wir einen runden Touch-Button, der am unteren Rand des Browserfensters eingeblendet wird. Später verwenden wir diesen Button, um die versteckte Ebene auf Touch-Geräten sichtbar zu machen.

Zunächst ergänzen wir nach dem Footer folgenden Button in unserer index.html:
// index.html

...

<button id="btn_clipPath" class="btn_clipPath">
  <span class="btn_inner">
    <div class="btn_image btn-ring">
      <img src="assets/images/text-ring.svg" alt="text-ring" />
    </div>
    <div class="btn_touch">
      <img src="assets/images/touch.svg" alt="touch" />
    </div>
  </span>
</button>

Der Button besteht aus zwei grafischen Ebenen. text-ring.svg bildet den äußeren Ring und touch.svg das mittig platzierte Touch-Symbol.

Damit sich der äußere Ring kontinuierlich dreht, erstellen wir zunächst eine CSS-Animation:
// assets/styles.css

@keyframes loop {
  from {
    -webkit-transform: rotate(0deg);
    transform: rotate(0deg);
  }
  to {
    -webkit-transform: rotate(360deg);
    transform: rotate(360deg);
  }
}

@-webkit-keyframes loop {
  from {
    -webkit-transform: rotate(0deg);
    transform: rotate(0deg);
  }
  to {
    -webkit-transform: rotate(360deg);
    transform: rotate(360deg);
  }
}

Die Animation dreht das Element von 0deg bis 360deg. Da wir sie anschließend unendlich wiederholen, entsteht eine kontinuierliche Kreisbewegung.

Nun gestalten und positionieren wir den Button:
// assets/styles.css

.btn_clipPath {
  background: none;
  border: none;
  bottom: 2.1875rem;
  left: calc(50% - 39px);
  padding: 0 !important;
  position: fixed;
  z-index: 4;
}

.btn_inner {
  display: block;
  position: relative;
}

.btn-ring {
  -webkit-animation: loop 10s infinite forwards linear;
  animation: loop 10s infinite forwards linear;
  height: 78px;
  width: 78px;
}

.btn_touch {
  -webkit-box-align: center;
  -ms-flex-align: center;
  -webkit-box-pack: center;
  -ms-flex-pack: center;
  align-items: center;
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  height: 100%;
  justify-content: center;
  left: 0;
  position: absolute;
  top: 0;
  width: 100%;
}

Mit position: fixed bleibt der Button unabhängig von der aktuellen Scrollposition am unteren Rand des Browserfensters.

Der äußere Ring besitzt eine Breite und Höhe von 78px. Mit
left: calc(50% - 39px);

ziehen wir die Hälfte dieser Breite von der Mitte des Browserfensters ab und positionieren den Button dadurch horizontal mittig.

.btn_inner dient als relativer Bezugspunkt für die beiden Grafiken. Das Touch-Symbol wird innerhalb von .btn_touchabsolut über dem Ring positioniert und mit Flexbox horizontal und vertikal zentriert.

Der äußere Ring erhält unsere zuvor angelegte loop-Animation:
animation: loop 10s infinite forwards linear;

Dadurch dreht sich text-ring.svg kontinuierlich, während das Touch-Symbol in der Mitte stehen bleibt.

Auf Desktop-Ansichten benötigen wir den Button nicht. Deshalb blenden wir ihn ab unserem Desktop-Breakpoint von 1248px aus:
// assets/styles.css

@media (min-width: 1248px) {
  .btn_clipPath {
    display: none;
  }
}

Damit ist der mobile Touch-Button vorbereitet. Unterhalb von 1248px wird er angezeigt, während er ab 1248pxausgeblendet bleibt.

Im nächsten Schritt verbinden wir den Button mit unserer Kreis-Maske und reagieren auf die Touch-Ereignisse.

TOUCH-STEUERUNG

Die Kreis-Maske per Touch ein- und ausblenden

Unser mobiler Button ist jetzt vorhanden. Im nächsten Schritt verbinden wir ihn mit der versteckten Inhaltsebene.

Berührt der Benutzer den Button, soll die Kreis-Maske eingeblendet werden. Wird die Berührung beendet, verschwindet sie wieder.

Dazu ergänzen wir unsere assets/scripts.js:
// assets/scripts.js

/* -------------------------------------------------------------------------- */
/*                              Touch-Steuerung                               */
/* -------------------------------------------------------------------------- */

const circleButton = document.getElementById("btn_clipPath");

circleButton.addEventListener("touchstart", (e) => {
  e.preventDefault();

  if (isMobileDevice()) {
    const touch = e.touches[0];

    gsap.set(hiddenContent, {
      autoAlpha: 1,
      "--x": touch.pageX,
      "--y": touch.pageY,
    });
  }
});

circleButton.addEventListener("touchend", (e) => {
  if (isMobileDevice()) {
    const touch = e.changedTouches[0];

    gsap.set(hiddenContent, {
      autoAlpha: 0,
      "--x": touch.pageX,
      "--y": touch.pageY,
    });

    gsap.to(hiddenContent, {
      "--size": 0,
      duration: 0.75,
      ease: "expo.out",
    });
  }
});

Zunächst holen wir uns den zuvor angelegten Button über seine ID:
const circleButton = document.getElementById("btn_clipPath");

Anschließend reagieren wir auf touchstart. Dieses Ereignis wird ausgelöst, sobald der Benutzer den Button mit dem Finger berührt.

Mit:
const touch = e.touches[0];

holen wir uns den ersten aktiven Berührungspunkt. Anders als bei einem Mausereignis lesen wir die Koordinaten eines Touch-Events nicht direkt über e.pageX und e.pageY aus.

Die Position des Fingers erhalten wir stattdessen über:
touch.pageX
touch.pageY

Diese Werte übergeben wir an unsere CSS-Variablen --x und --y. Gleichzeitig machen wir die versteckte Ebene mit autoAlpha: 1 sichtbar.

Mit e.preventDefault() verhindern wir während touchstart die Standardaktion des Browsers für diese Berührung.

Sobald der Benutzer den Finger wieder vom Button nimmt, wird touchend ausgelöst.

Zu diesem Zeitpunkt befindet sich der beendete Berührungspunkt nicht mehr in e.touches. Deshalb verwenden wir:
const touch = e.changedTouches[0];

Damit erhalten wir die Koordinaten der Berührung, die gerade beendet wurde.

Anschließend blenden wir die versteckte Ebene mit autoAlpha: 0 wieder aus und setzen die Position der Maske auf die letzte Touch-Position.

Zum Abschluss animieren wir die Größe der Kreis-Maske zurück auf 0:
gsap.to(hiddenContent, {
  "--size": 0,
  duration: 0.75,
  ease: "expo.out",
});

Damit besitzt unser Kreis-Effekt nun neben der Maussteuerung auf dem Desktop auch eine eigene Bedienung für kleinere Touch-Ansichten.


36.mp4 30 MB

Meld dich an und schreibe ein Kommentar