Wie man einen reinen CSS 3D-Paket-Toggle erstellt

Avatar of Jhey Tompkins
Jhey Tompkins am

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

Sie kennen doch diese Pappkartons, die komplett flach geliefert werden? Man faltet sie auf und klebt sie zu einer nützlichen Box zusammen. Wenn es dann Zeit ist, sie zu recyceln, schneidet man sie wieder auseinander, um sie flach zu legen. Kürzlich hat mich jemand wegen genau dieses Konzepts als 3D-Animation kontaktiert, und ich dachte, es wäre eine interessante Lektion, dies vollständig in CSS zu tun, also sind wir hier!

Wie könnte diese Animation aussehen? Wie könnten wir diesen Verpackungszeitplan erstellen? Könnte die Größe flexibel sein? Machen wir einen reinen CSS-Paket-Toggle.

Hier ist, worauf wir hinarbeiten. Tippen Sie, um den Pappkarton zu verpacken und auszupacken.

Wo fängt man an?

Wo fängt man so etwas an? Am besten plant man im Voraus. Wir wissen, dass wir eine Vorlage für unser Paket haben werden. Und diese muss sich in drei Dimensionen falten lassen. Wenn Ihnen 3D in CSS neu ist, empfehle ich diesen Artikel, um Ihnen den Einstieg zu erleichtern.

Wenn Sie mit 3D-CSS vertraut sind, mag es verlockend sein, einen Quader zu konstruieren und von dort aus weiterzumachen. Aber das wird einige Probleme mit sich bringen. Wir müssen berücksichtigen, wie ein Paket von 2D zu 3D wird.

Beginnen wir mit der Erstellung einer Vorlage. Wir müssen im Voraus mit unserem Markup planen und darüber nachdenken, wie wir unsere Verpackungsanimation gestalten wollen. Beginnen wir mit etwas HTML.

<div class="scene">
  <div class="package__wrapper">
    <div class="package">
      <div class="package__side package__side--main">
        <div class="package__flap package__flap--top"></div>
        <div class="package__flap package__flap--bottom"></div>
        <div class="package__side package__side--tabbed">
          <div class="package__flap package__flap--top"></div>
          <div class="package__flap package__flap--bottom"></div>
        </div>
        <div class="package__side package__side--extra">
          <div class="package__flap package__flap--top"></div>
          <div class="package__flap package__flap--bottom"></div>
          <div class="package__side package__side--flipped">
            <div class="package__flap package__flap--top"></div>
            <div class="package__flap package__flap--bottom"></div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

Mixins sind eine gute Idee

Da passiert einiges. Es sind viele divs. Ich verwende gerne Pug, um Markup zu generieren, damit ich Dinge in wiederverwendbare Blöcke aufteilen kann. Zum Beispiel hat jede Seite zwei Klappen. Wir können einen Pug-Mixin für die Seiten erstellen und Attribute verwenden, um einen Modifikator-Klassennamen anzuwenden, um all dieses Markup viel einfacher zu schreiben.

mixin flaps()
  .package__flap.package__flap--top
  .package__flap.package__flap--bottom
      
mixin side()
  .package__side(class=`package__side--${attributes.class || 'side'}`)
    +flaps()
    if block
      block

.scene
  .package__wrapper
    .package
      +side()(class="main")
        +side()(class="tabbed")
        +side()(class="extra")
          +side()(class="flipped")

Wir verwenden zwei Mixins. Einer erstellt die Klappen für jede Seite der Box. Der andere erstellt die Seiten der Box. Beachten Sie im side-Mixin, dass wir block verwenden. Dort werden die Kinder der Mixin-Nutzung gerendert, was besonders nützlich ist, da wir einige der Seiten verschachteln müssen, um uns das Leben später leichter zu machen.

Unser generiertes Markup

<div class="scene">
  <div class="package__wrapper">
    <div class="package">
      <div class="package__side package__side--main">
        <div class="package__flap package__flap--top"></div>
        <div class="package__flap package__flap--bottom"></div>
        <div class="package__side package__side--tabbed">
          <div class="package__flap package__flap--top"></div>
          <div class="package__flap package__flap--bottom"></div>
        </div>
        <div class="package__side package__side--extra">
          <div class="package__flap package__flap--top"></div>
          <div class="package__flap package__flap--bottom"></div>
          <div class="package__side package__side--flipped">
            <div class="package__flap package__flap--top"></div>
            <div class="package__flap package__flap--bottom"></div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

Verschachtelung der Seiten

Das Verschachteln der Seiten erleichtert das Zusammenfalten unseres Pakets. Ähnlich wie jede Seite zwei Klappen hat. Die Kinder einer Seite können die Transformation der Seite erben und dann ihre eigene anwenden. Wenn wir mit einem Quader begonnen hätten, wäre es schwierig, dies zu nutzen.

Screenshot showing HTML markup on the left and a rendering the unfolded cardboard box on the right. The markup shows how one of the box’s sides is a parent container that sets the broad side of the box and contains children for the corresponding top and bottom flaps. Orange arrows connect each element to the visual rendering to outline which parts of the box correspond in HTML correspond to the visual rendering.

Schauen Sie sich diese Demo an, die zwischen verschachtelten und nicht verschachtelten Elementen wechselt, um den Unterschied in Aktion zu sehen.

Jede Box hat einen transform-origin, der auf die untere rechte Ecke mit 100% 100% gesetzt ist. Wenn Sie den "Transform"-Schalter aktivieren, dreht sich jede Box um 90 Grad. Aber sehen Sie, wie sich das Verhalten dieser transform ändert, wenn wir die Elemente verschachteln.

Wir wechseln zwischen den beiden Versionen des Markups, ändern aber nichts anderes.

Verschachtelt

<div class="boxes boxes--nested">
  <div class="box">
    <div class="box">
      <div class="box">
        <div class="box"></div>
      </div>
    </div>
  </div>
</div>

Nicht verschachtelt

<div class="boxes">
  <div class="box"></div>
  <div class="box"></div>
  <div class="box"></div>
  <div class="box"></div>
</div>

Alles transformieren

Nachdem wir einige Stile auf unser HTML angewendet haben, haben wir unsere Paketvorlage.

Die Stile legen die verschiedenen Farben fest und positionieren die Seiten relativ zum Paket. Jede Seite erhält eine Position, die relativ zur "Hauptseite" ist. (Sie werden gleich sehen, warum all diese Verschachtelungen nützlich sind.)

Es gibt einige Dinge zu beachten. Ähnlich wie bei der Arbeit mit Quadern verwenden wir die Variablen --height, --width und --depth für die Größenbestimmung. Dies erleichtert die spätere Änderung der Paketgröße.

.package {
  height: calc(var(--height, 20) * 1vmin);
  width: calc(var(--width, 20) * 1vmin);
}

Warum die Größenbestimmung so definieren? Wir verwenden eine einheitenlose Standardgröße von 20, eine Idee, die ich von Lea Verous 2016 CSS ConfAsia Rede (ab 52:44) übernommen habe. Indem wir benutzerdefinierte Eigenschaften als "Daten" anstelle von "Werten" verwenden, können wir mit calc() tun, was wir wollen. Zusätzlich muss JavaScript sich nicht um Einheiten kümmern und wir können zu Pixel, Prozent usw. wechseln, ohne woanders Änderungen vornehmen zu müssen. Sie könnten dies in einen Koeffizienten in --root refaktorieren, aber es könnte auch schnell übertrieben werden.

Die Klappen jeder Seite müssen auch etwas kleiner sein als die Seiten, zu denen sie gehören. Dies geschieht, damit wir einen leichten Spalt sehen, wie wir ihn im wirklichen Leben hätten. Außerdem müssen die Klappen an zwei Seiten etwas tiefer sitzen. Das verhindert, dass es beim Hochklappen zu z-index-Konflikten zwischen ihnen kommt.

.package__flap {
  width: 99.5%;
  height: 49.5%;
  background: var(--flap-bg, var(--face-4));
  position: absolute;
  left: 50%;
  transform: translate(-50%, 0);
}
.package__flap--top {
  transform-origin: 50% 100%;
  bottom: 100%;
}
.package__flap--bottom {
  top: 100%;
  transform-origin: 50% 0%;
}
.package__side--extra > .package__flap--bottom,
.package__side--tabbed > .package__flap--bottom {
  top: 99%;
}
.package__side--extra > .package__flap--top,
.package__side--tabbed > .package__flap--top {
  bottom: 99%;
}

Wir beginnen auch, den transform-origin für die einzelnen Teile zu berücksichtigen. Eine obere Klappe dreht sich von ihrer unteren Kante und eine untere Klappe von ihrer oberen Kante.

Wir können ein Pseudo-Element für die Lasche auf der rechten Seite verwenden. Wir verwenden clip-path, um die gewünschte Form zu erhalten.

.package__side--tabbed:after {
  content: '';
  position: absolute;
  left: 99.5%;
  height: 100%;
  width: 10%;
  background: var(--face-3);
  -webkit-clip-path: polygon(0 0%, 100% 20%, 100% 80%, 0 100%);
  clip-path: polygon(0 0%, 100% 20%, 100% 80%, 0 100%);
  transform-origin: 0% 50%;
}

Beginnen wir mit der Arbeit an unserer Vorlage auf einer 3D-Ebene. Wir können damit beginnen, die .scene auf der X- und Y-Achse zu drehen.

.scene {
  transform: rotateX(-24deg) rotateY(-32deg) rotateX(90deg);
}

Zusammenfalten

Wir sind bereit, unsere Vorlage zusammenzufalten! Unsere Vorlage faltet sich basierend auf einer benutzerdefinierten Eigenschaft --packaged. Wenn der Wert 1 ist, können wir die Vorlage zusammenfalten. Falten wir zum Beispiel einige der Seiten und die Pseudo-Element-Lasche.

.package__side--tabbed,
.package__side--tabbed:after {
  transform: rotateY(calc(var(--packaged, 0) * -90deg)); 
}
.package__side--extra {
  transform: rotateY(calc(var(--packaged, 0) * 90deg));
}

Oder wir könnten eine Regel für alle Seiten schreiben, die nicht die "Hauptseite" sind.

.package__side:not(.package__side--main),
.package__side:not(.package__side--main):after {
  transform: rotateY(calc((var(--packaged, 0) * var(--rotation, 90)) * 1deg));
}
.package__side--tabbed { --rotation: -90; }

Und das würde alle Seiten abdecken.

Erinnern Sie sich, als ich sagte, dass die verschachtelten Seiten es uns erlauben, die Transformation eines Elternteils zu erben? Wenn wir unsere Demo so ändern, dass wir den Wert von --packaged ändern können, können wir sehen, wie der Wert die Transformationen beeinflusst. Versuchen Sie, den Wert von --packaged zwischen 1 und 0 zu schieben, und Sie werden genau verstehen, was ich meine.

Jetzt, da wir eine Möglichkeit haben, den Faltzuszstand unserer Vorlage umzuschalten, können wir mit etwas Bewegung beginnen. Unsere vorherige Demo schaltet zwischen den beiden Zuständen. Wir können transition dafür verwenden. Der schnellste Weg? Fügen Sie eine transition zur transform jedes Kindes in der .scene hinzu.

.scene *,
.scene *::after {
  transition: transform calc(var(--speed, 0.2) * 1s);
}

Mehrstufige Übergänge!

Aber wir falten die Vorlage nicht auf einmal zusammen – im wirklichen Leben gibt es eine Reihenfolge, bei der wir zuerst eine Seite und ihre Klappe falten und dann zur nächsten übergehen, und so weiter. Gekapselte benutzerdefinierte Eigenschaften sind perfekt dafür.

.scene *,
.scene *::after {
  transition: transform calc(var(--speed, 0.2) * 1s) calc((var(--step, 1) * var(--delay, 0.2)) * 1s);
}

Hier sagen wir, dass für jeden transition eine transition-delay von --step multipliziert mit --delay verwendet wird. Der Wert --delay ändert sich nicht, aber jedes Element kann definieren, in welchem "Schritt" es sich in der Sequenz befindet. Und dann können wir die Reihenfolge, in der die Dinge passieren, explizit festlegen.

.package__side--extra {
  --step: 1;
}
.package__side--tabbed {
  --step: 2;
}
.package__side--flipped,
.package__side--tabbed::after {
  --step: 3;
}

Betrachten Sie die folgende Demo, um eine bessere Vorstellung davon zu bekommen, wie das funktioniert. Ändern Sie die Schiebereglerwerte, um die Reihenfolge der Ereignisse zu ändern. Können Sie ändern, welches Auto gewinnt?

Die gleiche Technik ist entscheidend für das, was wir vorhaben. Wir könnten sogar ein --initial-delay einführen, das allem eine leichte Pause hinzufügt, für noch mehr Realismus.

.race__light--animated,
.race__light--animated:after,
.car {
  animation-delay: calc((var(--step, 0) * var(--delay-step, 0)) * 1s);
}

Wenn wir uns unser Paket noch einmal ansehen, können wir dies weiter ausbauen und einen "Schritt" auf alle Elemente anwenden, die transformieren sollen. Es ist ziemlich wortreich, aber es erfüllt seinen Zweck. Alternativ könnten Sie diese Werte direkt im Markup einfügen.

.package__side--extra > .package__flap--bottom {
  --step: 4;
}
.package__side--tabbed > .package__flap--bottom {
  --step: 5;
}
.package__side--main > .package__flap--bottom {
  --step: 6;
}
.package__side--flipped > .package__flap--bottom {
  --step: 7;
}
.package__side--extra > .package__flap--top {
  --step: 8;
}
.package__side--tabbed > .package__flap--top {
  --step: 9;
}
.package__side--main > .package__flap--top {
  --step: 10;
}
.package__side--flipped > .package__flap--top {
  --step: 11;
}

Aber es fühlt sich nicht sehr realistisch an.

Vielleicht sollten wir die Box auch umdrehen

Wenn ich die Box im wirklichen Leben falten würde, würde ich die Box wahrscheinlich hochklappen, bevor ich die oberen Klappen einklappe. Wie könnten wir das machen? Nun, die mit scharfen Augen haben vielleicht das Element .package__wrapper bemerkt. Wir werden dieses verwenden, um das Paket zu verschieben. Dann werden wir das Paket auf der x-Achse drehen. Das erzeugt den Eindruck, das Paket auf die Seite zu kippen.

.package {
  transform-origin: 50% 100%;
  transform: rotateX(calc(var(--packaged, 0) * -90deg));
}
.package__wrapper {
  transform: translate(0, calc(var(--packaged, 0) * -100%));
}

Wenn wir die --step-Deklarationen entsprechend anpassen, erhalten wir etwas wie das hier.

Auspacken der Box

Wenn Sie zwischen dem gefalteten und dem nicht gefalteten Zustand wechseln, werden Sie feststellen, dass das Auspacken nicht richtig aussieht. Die Entfaltsequenz sollte genau das Gegenteil der Faltsequenz sein. Wir könnten die --step basierend auf --packaged und der Anzahl der Schritte umkehren. Unser letzter Schritt ist 15. Wir können unsere transition auf diesen Wert aktualisieren:

.scene *,
.scene *:after {
  --no-of-steps: 15;
  --step-delay: calc(var(--step, 1) - ((1 - var(--packaged, 0)) * (var(--step) - ((var(--no-of-steps) + 1) - var(--step)))));
  transition: transform calc(var(--speed, 0.2) * 1s) calc((var(--step-delay) * var(--delay, 0.2)) * 1s);
}

Das ist eine ziemliche Handvoll calc, um die transition-delay umzukehren. Aber es funktioniert! Wir müssen uns daran erinnern, diesen Wert --no-of-steps aktuell zu halten!

Wir haben noch eine weitere Option. Während wir auf dem Weg zur "reinen CSS"-Route bleiben, werden wir schließlich den Checkbox-Hack verwenden, um zwischen den Faltzusammenhängen umzuschalten. Wir könnten zwei Sätze definierter "Schritte" haben, bei denen ein Satz aktiv ist, wenn unsere Checkbox aktiviert wird. Es ist sicherlich eine umständlichere Lösung. Aber sie gibt uns mehr Feinsteuerung.

/* Folding */
:checked ~ .scene .package__side--extra {
  --step: 1;
}
/* Unfolding */
.package__side--extra {
  --step: 15;
}

Größe und Zentrierung

Bevor wir die Verwendung von [dat.gui](https://github.com/dataarts/dat.gui) aus unserer Demo entfernen, spielen wir mit der Größe unseres Pakets herum. Wir wollen prüfen, ob unser Paket beim Falten und Drehen zentriert bleibt. In dieser Demo hat das Paket eine größere --height und die .scene hat einen gestrichelten Rahmen.

Wir können auch unsere transform anpassen, um das Paket besser zu zentrieren, während wir schon dabei sind.

/* Considers package height by translating on z-axis */
.scene {
  transform: rotateX(calc(var(--rotate-x, -24) * 1deg)) rotateY(calc(var(--rotate-y, -32) * 1deg)) rotateX(90deg) translate3d(0, 0, calc(var(--height, 20) * -0.5vmin));
}
/* Considers package depth by sliding the depth before flipping */
.package__wrapper {
  transform: translate(0, calc((var(--packaged, 0) * var(--depth, 20)) * -1vmin));
}

Dies sorgt für eine zuverlässige Zentrierung im Bild. Es kommt jedoch alles auf die Vorliebe an!

Hinzufügen des Checkbox-Hacks

Jetzt räumen wir dat.gui beiseite und machen das Ganze "rein" CSS. Dazu müssen wir eine Reihe von Steuerelementen im HTML einführen. Wir verwenden eine Checkbox zum Auf- und Zuklappen unseres Pakets. Dann verwenden wir ein radio-Button, um eine Paketgröße auszuwählen.

<input id="package" type="checkbox"/>

<input id="one" type="radio" name="size"/>
<label class="size-label one" for="one">S</label>

<input id="two" type="radio" name="size" checked="checked"/>
<label class="size-label two" for="two">M</label>

<input id="three" type="radio" name="size"/>
<label class="size-label three" for="three">L</label>

<input id="four" type="radio" name="size"/>
<label class="size-label four" for="four">XL</label>

<label class="close" for="package">Close Package</label>
<label class="open" for="package">Open Package</label>

In der finalen Demo werden wir die Eingaben ausblenden und die Label-Elemente verwenden. Vorerst lassen wir sie alle sichtbar. Der Trick besteht darin, den Geschwisterkombinator (~) zu verwenden, wenn bestimmte Steuerelemente :checked sind. Wir können dann benutzerdefinierte Eigenschaftswerte für .scene festlegen.

#package:checked ~ .scene {
  --packaged: 1;
}
#one:checked ~ .scene {
  --height: 10;
  --width: 20;
  --depth: 20;
}
#two:checked ~ .scene {
  --height: 20;
  --width: 20;
  --depth: 20;
}
#three:checked ~ .scene {
  --height: 20;
  --width: 30;
  --depth: 20;
}
#four:checked ~ .scene {
  --height: 30;
  --width: 20;
  --depth: 30;
}

Und hier ist die Demo, die das leistet!

Finale Politur

Jetzt sind wir an einem Punkt, an dem wir Dinge "hübsch" machen und einige zusätzliche Details hinzufügen können. Beginnen wir damit, alle Eingaben auszublenden.

input {
  position: fixed;
  top: 0;
  left: 0;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border-width: 0;
}

Wir können die Größenoptionen als abgerundete Buttons gestalten.

.size-label {
  position: fixed;
  top: var(--top);
  right: 1rem;
  z-index: 3;
  font-family: sans-serif;
  font-weight: bold;
  color: #262626;
  height: 44px;
  width: 44px;
  display: grid;
  place-items: center;
  background: #fcfcfc;
  border-radius: 50%;
  cursor: pointer;
  border: 4px solid #8bb1b1;
  transform: translate(0, calc(var(--y, 0) * 1%)) scale(var(--scale, 1));
  transition: transform 0.1s;
}
.size-label:hover {
  --y: -5;
}
.size-label:active {
  --y: 2;
  --scale: 0.9;
}

Wir möchten überall tippen können, um das Falten und Entfalten unseres Pakets umzuschalten. Daher werden unsere .open- und .close-Labels den gesamten Bildschirm einnehmen. Fragen Sie sich, warum wir zwei Labels haben? Das ist ein kleiner Trick. Wenn wir eine transition-delay verwenden und das entsprechende Label skalieren, können wir beide Labels ausblenden, während das Paket sich übergangt. So bekämpfen wir Spam-Tippen (auch wenn es einen Benutzer nicht davon abhält, die Leertaste auf einer Tastatur zu drücken).

.close,
.open {
  position: fixed;
  height: 100vh;
  width: 100vw;
  z-index: 2;
  transform: scale(var(--scale, 1)) translate3d(0, 0, 50vmin);
  transition: transform 0s var(--reveal-delay, calc(((var(--no-of-steps, 15) + 1) * var(--delay, 0.2)) * 1s));
}

#package:checked ~ .close,
.open {
  --scale: 0;
  --reveal-delay: 0s;
}
#package:checked ~ .open {
  --scale: 1;
  --reveal-delay: calc(((var(--no-of-steps, 15) + 1) * var(--delay, 0.2)) * 1s);
}

Schauen Sie sich diese Demo an, um zu sehen, wo wir background-color zu beiden .open und .close hinzugefügt haben. Keines der Labels ist während des Übergangs sichtbar.

Wir haben vollständige Funktionalität! Aber unser Paket ist im Moment etwas unscheinbar. Fügen wir zusätzliche Details hinzu, um es "kistenartiger" zu machen, mit Dingen wie Paketband und Versandetiketten.

Kleine Details wie diese sind nur durch unsere Vorstellungskraft begrenzt! Wir können unsere benutzerdefinierte Eigenschaft --packaged verwenden, um alles zu beeinflussen. Zum Beispiel übergangt das .package__tape die scaleY-Transformation.

.package__tape {
  transform: translate3d(-50%, var(--offset-y), -2px) scaleX(var(--packaged, 0));
}

Das zu Beachtende ist, dass wir jedes Mal, wenn wir eine neue Funktion hinzufügen, die die Reihenfolge beeinflusst, unsere Schritte aktualisieren müssen. Nicht nur die --step-Werte, sondern auch der Wert --no-of-steps.

Das ist alles!

So erstellt man einen reinen CSS 3D-Paket-Toggle. Werden Sie das in Ihre Website einbauen? Unwahrscheinlich! Aber es macht Spaß zu sehen, wie man solche Dinge mit CSS erreichen kann. Benutzerdefinierte Eigenschaften sind so leistungsfähig.

Warum nicht super festlich werden und das Geschenk von CSS machen!

Bleib fantastisch! ʕ •ᴥ•ʔ