Vereinfachung von CSS-Würfeln mit benutzerdefinierten Eigenschaften

Avatar of Ana Tudor
Ana Tudor on

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

Ich weiß, dass es da draußen unzählige reine CSS-Würfel-Tutorials gibt. Ich habe selbst schon ein paar gemacht. Aber Mitte 2017, wenn CSS Custom Properties in allen wichtigen Desktop-Browsern unterstützt werden, fühlen sich all diese veraltet und sehr WET an. Ich dachte, ich müsste etwas tun, um dieses Problem zu beheben, also entstand dieser Artikel. Er wird Ihnen den effizientesten Weg zum Erstellen eines CSS-Würfels aufzeigen, der heute möglich ist, und gleichzeitig erklären, welche üblichen, aber weniger idealen Cube-Codierungsmuster Sie vermeiden sollten. Also fangen wir an!

HTML-Struktur

Die HTML-Struktur ist die folgende: ein .cube-Element mit .cube__face-Kindern (6 davon). Wir verwenden Haml, damit wir möglichst wenig Code schreiben müssen.

.cube
  - 6.times do
    .cube__face

Wir verwenden keine Klassen wie .front, .back und ähnliche. Sie sind nicht nützlich, da sie den Code aufblähen und ihn weniger logisch machen. Stattdessen verwenden wir :nth-child(), um die Flächen anzusprechen. Wir müssen uns keine Sorgen um die Browserunterstützung dafür machen, da wir etwas mit 3D-Transformationen erstellen, was eine viel neuere Browserunterstützung voraussetzt!

Grundlegende Stile

Alle diese Elemente sind absolut positioniert.

[class*='cube'] { position: absolute }

Das .cube ist das Kind eines Szenenelements, das in unserem Fall das body ist, da wir die Dinge so einfach wie möglich halten wollen. Hätten wir mehrere 3D-Formen innerhalb der Szene und wollten, dass sie auf 3D- Weise interagieren, dann wäre unser Würfel ein Kind dieser Anordnung und die Anordnung ein Kind der Szene.

Wir lassen den body den gesamten Viewport abdecken und setzen eine perspective darauf, damit alles, was näher ist, größer und alles, was weiter weg ist, kleiner erscheint.

body {
  height: 100vh;
  perspective: 25em
}

Etwas anderes, was ich oft gerne mache, wenn der vollhohe body die Szene ist, ist, die font-size auf dem .cube so einzustellen, dass sie von der minimalen Viewport-Dimension abhängt. Das lässt unseren gesamten Würfel gut mit dem Viewport skalieren, wenn ich dann die Cube-Dimensionen in em-Einheiten setze.

.cube { font-size: 8vmin }

Der Grund, warum ich die Cube-Dimensionen nicht direkt in vmin-Einheiten setze, ist ein Edge-Bug.

Wir geben dem .cube-Element dann eine transform-style von preserve-3d, damit seine Cube-Kinder nicht in seine Ebene abgeflacht werden, falls wir uns entscheiden, es zu animieren, und wir positionieren es mit top- und left-Offsets in der Mitte der Szene. Dies ist die anfängliche Positionierung des Würfels und es ist am besten, Offsets zu verwenden, keine translate()-Transformation dafür. Ich habe gesehen, dass Leute sich manchmal darüber verwirren, weil sie gehört haben, dass es aus Performance-Gründen besser ist, Transformationen zu verwenden, keine Offsets... das stimmt, aber es gilt für die Animation der Position, nicht für die anfängliche Positionierung. Die sehr einfache Regel hier ist: Verwenden Sie Offsets oder Margins, was auch immer zu diesem Zeitpunkt für die anfängliche Positionierung bequemer ist, verwenden Sie Transformationen für die Animation der Position, die von dieser anfänglichen Position ausgeht.

.cube {
  top: 50%; left: 50%;
  transform-style: preserve-3d;
}

Wir wählen dann eine Kantenlänge für den Würfel und setzen sie als width und height der Würfelflächen. Wir geben den Flächen auch einen negativen Margin von der halben Würfelkante, damit sie genau in der Mitte sind. Wiederum ist das mit der anfänglichen Positionierung der Würfelflächen verbunden. Wir geben ihnen auch einen box-shadow, nur damit wir sie sehen können.

$cube-edge: 8em;

.cube__face {
  margin: -.5*$cube-edge;
  width: $cube-edge; height: $cube-edge;
  box-shadow: 0 0 0 2px;
}

Ich sehe oft Code, bei dem transform-style: preserve-3d auf alles gesetzt wurde. Das ist unnötig und ein Missverständnis, wie preserve-3d funktioniert. Es ist nur notwendig, es auf etwas zu setzen, das eine 3D-Transformation erhalten wird (sofort, nach Benutzerinteraktion, über eine automatisch laufende Animation ... spielt keine Rolle wie) *und* 3D-transformierte Kinder hat. In unserem speziellen Fall ist das nur das .cube-Element. Die Szene wird nicht in 3D transformiert und die .cube__face-Elemente haben keine Kinder.

Eine weitere unnötige Sache, die ich sehe, ist das Setzen expliziter Dimensionen auf dem .cube-Element. Dieses Element ist nicht sichtbar. Wir haben keinen Text direkt darin, wir setzen keine Hintergründe, Rahmen oder Schatten darauf. Sein einziger Zweck hier ist, als Container zu dienen, dessen Position wir animieren können, um alle seine Flächenkinder gleichzeitig, auf die gleiche Weise zu bewegen. Wenn keine Dimensionen auf diesem absolut positionierten .cube-Element gesetzt werden, werden seine Dimensionen auf 0x0 berechnet, daher ist es auch sinnlos, %-basierte Offsets auf seine Flächenkinder zu setzen. top: 0 ist exakt dasselbe wie top: 50% oder jeder andere Prozentwert für ein Element, dessen Elternteil 0x0 Dimensionen hat. Das Gleiche gilt für alle anderen Offsets (right, bottom, left).

Mir wurde gefragt, warum ich top und left für das .cube nicht auf calc(50% - #{.5*$cube-edge}) setze und den margin von .cube__face komplett entferne, wenn ich so viel Wert auf kompakten Code lege. Nun, das liegt daran, dass die beiden nicht wirklich dasselbe Ergebnis liefern, obwohl die .cube__face-Elemente in beiden Fällen in der Mitte des Bildschirms landen. Um das zu veranschaulichen, geben wir unserem .cube-Element einen roten box-shadow, nur damit wir ihn sehen können, und vergleichen die beiden Fälle nebeneinander.

Sehen Sie den Pen von thebabydino (@thebabydino) auf CodePen.

In der obigen Demo wird unser .cube-Element in den beiden Fällen unterschiedlich positioniert. Wenn der calc()-Wert für seine Offsets verwendet wird und der Margin seiner Kinder übersprungen wird, stimmt seine Position nicht mehr mit der Mitte der Szene überein, sondern mit der oberen linken Ecke seiner Flächenkinder. Na und? Er wird in unserer eigentlichen Demo sowieso nicht sichtbar sein...

Während das stimmt, bedeutet eine andere Position auch ein anderes transform-origin. Und das ändert die Dinge, wenn wir uns entscheiden, unseren .cube zu drehen oder zu skalieren (und das ist etwas, das wir tun wollten). Betrachten Sie also die folgende Keyframe-Animation für unseren Würfel.

@keyframes rot { to { transform: rotateY(1turn) } }

Dies ist eine Drehung um die y-Achse des Würfels. Das Ergebnis ist für beide Fälle nicht dasselbe.

Sehen Sie den Pen von thebabydino (@thebabydino) auf CodePen.

In beiden Fällen drehen sich die Flächen um die y-Achse ihres Eltern-Würfels, aber die Position dieser y-Achse relativ zu den Flächen ist unterschiedlich. Sie stimmt in der anfänglichen Situation mit den y-Achsen der Flächen überein und im zweiten Fall mit den linken Kanten der Flächen. Das ist der Grund, warum ich den negativen Margin der Würfelflächen nicht in die Offsets des Eltern-Würfels einbeziehe: es würde die Animation des Würfels in 3D beeinträchtigen.

Den Würfel mit Transformationen erstellen

Was wir in den obigen Demos haben, ist noch kein Würfel. Um das zu erreichen, müssen wir die Flächen im 3D-Raum positionieren. Es gibt mehrere Transformationskombinationen, die den gleichen Effekt erzielen, aber die effizienteste und logischste ist, damit zu beginnen, die ersten vier Flächen in 90°-Schritten um eine der Achsen in ihrer Ebene (x oder y) zu drehen und die verbleibenden zwei Flächen um ±90° um die andere Achse in derselben Ebene. Dann koppeln wir eine Translation von der halben Würfelkantenlänge entlang der Achse, die senkrecht zu ihrer Ebene steht (ihrer z-Achse).

Eine sehr detaillierte Erklärung, wie Translationen und Rotationen funktionieren und wie wir die transform-Ketten zum Erstellen eines Quaders erhalten, finden Sie in diesem älteren Artikel. Der Fall eines Würfels ist eine vereinfachte Version, bei der alle Abmessungen entlang der drei Achsen gleich sind.

Wenn wir uns dafür entscheiden, die ersten vier Flächen um ihre y-Achsen zu drehen, sehen unsere transform-Ketten wie folgt aus.

.cube__face:nth-child(1) {
  transform: rotateY(  0deg) translateZ(.5*$cube-edge)
}
.cube__face:nth-child(2) {
  transform: rotateY( 90deg) translateZ(.5*$cube-edge)
}
.cube__face:nth-child(3) {
  transform: rotateY(180deg) translateZ(.5*$cube-edge)
}
.cube__face:nth-child(4) {
  transform: rotateY(270deg) translateZ(.5*$cube-edge)
}
.cube__face:nth-child(5) {
  transform: rotateX( 90deg) translateZ(.5*$cube-edge)
}
.cube__face:nth-child(6) {
  transform: rotateX(-90deg) translateZ(.5*$cube-edge)
}

Jetzt ersetzen wir die rotateY(ay)- und rotateX(ax)-Komponenten durch ihre rotate3d(i, j, k, a)-Äquivalente. Die i, j und k in der rotate3d()-Funktion sind die Komponenten des Einheitsvektors der Rotationsachse entlang der x-, y- und z-Koordinatenachsen, während a der Rotationswinkel um diese Rotationsachse ist.

Da die Rotationsachse im Falle einer rotateY() die y-Achse ist, sind die Komponenten des Einheitsvektors entlang der beiden anderen Achsen (i entlang der x-Achse und k entlang der z-Achse) 0, während die Komponente entlang der y-Achse (j) 1 ist. Außerdem ist a in diesem Fall ay.

Ähnlich haben wir im Falle einer rotateX(), dass i 1 ist, j und k 0 sind und a ax ist. Unsere äquivalenten Ketten mit rotate3d wären also.

.cube__face:nth-child(1) {
  transform: rotate3d(0 /* i */, 1 /* j */, 0 /* k */,   0deg /*  0*90° */) 
    translateZ(.5*$cube-edge)
}
.cube__face:nth-child(2) {
  transform: rotate3d(0 /* i */, 1 /* j */, 0 /* k */,  90deg /*  1*90° */) 
    translateZ(.5*$cube-edge)
}
.cube__face:nth-child(3) {
  transform: rotate3d(0 /* i */, 1 /* j */, 0 /* k */, 180deg /*  2*90° */) 
    translateZ(.5*$cube-edge)
}
.cube__face:nth-child(4) {
  transform: rotate3d(0 /* i */, 1 /* j */, 0 /* k */, 270deg /*  3*90° */) 
    translateZ(.5*$cube-edge)
}
.cube__face:nth-child(5) {
  transform: rotate3d(1 /* i */, 0 /* j */, 0 /* k */,  90deg /*  1*90° */) 
    translateZ(.5*$cube-edge)
}
.cube__face:nth-child(6) {
  transform: rotate3d(1 /* i */, 0 /* j */, 0 /* k */, -90deg /* -1*90° */) 
    translateZ(.5*$cube-edge)
}

Wir bemerken einige Dinge im obigen Code. Erstens ist die k-Komponente immer 0. Dann ist die i-Komponente 0 für die ersten vier Flächen und 1 für die letzten beiden, während die j-Komponente 1 für die ersten vier Flächen und 0 für die letzten beiden ist. Schließlich kann der Winkelwert immer als Multiplikator mal 90° geschrieben werden.

Das bedeutet, wir können CSS-Variablen in unseren Code einführen, damit wir diese Transformationsfunktionen nicht wiederholen müssen.

.cube__face {
  transform: rotate3d(var(--i), var(--j), 0, calc(var(--m)*90deg)) 
    translateZ(.5*$cube-edge);
	
  &:nth-child(1) { --i: 0; --j: 1; --m:  0; }
  &:nth-child(2) { --i: 0; --j: 1; --m:  1; }
  &:nth-child(3) { --i: 0; --j: 1; --m:  2; }
  &:nth-child(4) { --i: 0; --j: 1; --m:  3; }
  &:nth-child(5) { --i: 1; --j: 0; --m:  1; }
  &:nth-child(6) { --i: 1; --j: 0; --m: -1; }
}

Da sowohl --i als auch --j denselben Wert für die ersten vier Flächen behalten und nur für die letzten beiden einen anderen Wert erhalten, können wir ihre Standardwerte auf 0 bzw. 1 setzen und sie dann für die Flächen 5 und 6 auf 1 bzw. 0 umschalten. Diese beiden Flächen können mit :nth-child(n + 5) ausgewählt werden. Außerdem können wir den Standardwert für --m auf 0 setzen und so die Notwendigkeit der :nth-child(1)-Regel vollständig beseitigen.

.cube__face {
  transform: rotate3d(var(--i, 0), var(--j, 1), 0, calc(var(--m, 0)*90deg)) 
    translateZ(.5*$cube-edge);
	
  &:nth-child(n + 5) { --i: 1; --j: 0 }

  &:nth-child(2 /* 2 = 1 + 1 */) { --m:  1 }
  &:nth-child(3 /* 3 = 2 + 1 */) { --m:  2 }
  &:nth-child(4 /* 4 = 3 + 1 */) { --m:  3 }
  &:nth-child(5 /* 5 = 4 + 1 */) { --m:  1 /*  1 = pow(-1, 4) */ }
  &:nth-child(6 /* 6 = 5 + 1 */) { --m: -1 /* -1 = pow(-1, 5) */ }
}

Wenn wir die Dinge weiter vorantreiben, stellen wir fest, dass, ob 1 oder 0, --j durch calc(1 - var(--i)) ersetzt werden kann und dass --m entweder der Flächenindex für die ersten vier Flächen oder -1 hoch den Flächenindex für die letzten beiden Flächen ist. Das erlaubt uns, die Variable --j zu eliminieren und den Multiplikator --m in einer Schleife zu setzen.

.cube__face {
  --i: 0;
  transform: rotate3d(var(--i), calc(1 - var(--i)), 0, calc(var(--m, 0)*90deg)) 
    translateZ(.5*$cube-edge);
  
  &:nth-child(n + 5) { --i: 1 }
  
  @for $f from 1 to 6 {
    &:nth-child(#{$f + 1}) { --m: if($f < 4, $f, pow(-1, $f)) }
  }
}

Das Ergebnis ist unten zu sehen.

Black cube wireframe.
Der statische Würfel (Live-Demo).

Der größte Unterschied besteht hier in Bezug auf den kompilierten Code. Mit dieser CSS-Variablen-Methode schreiben wir die Transformationsfunktionen nur einmal.

.cube__face {
  --i: 0;
  transform: rotate3d(var(--i), calc(1 - var(--i)), 0, calc(var(--m, 0)*90deg)) 
    translateZ(4em);
}

.cube__face:nth-child(n + 5) { --i: 1 }

.cube__face:nth-child(2) { --m: 1 }
.cube__face:nth-child(3) { --m: 2 }
.cube__face:nth-child(4) { --m: 3 }
.cube__face:nth-child(5) { --m: 1 }
.cube__face:nth-child(6) { --m: -1 }

Ohne CSS-Variablen hätten wir das Beste getan, indem wir die Transformationsfunktionen für jede einzelne Fläche wiederholen müssten.

.cube__face:nth-child(1) {
  transform: rotateY(0deg) translateZ(4em)
}
.cube__face:nth-child(2) {
  transform: rotateY(90deg) translateZ(4em)
}
.cube__face:nth-child(3) {
  transform: rotateY(180deg) translateZ(4em)
}
.cube__face:nth-child(4) {
  transform: rotateY(270deg) translateZ(4em)
}
.cube__face:nth-child(5) {
  transform: rotateX(90deg) translateZ(4em)
}
.cube__face:nth-child(6) {
  transform: rotateX(-90deg) translateZ(4em)
}

Den Würfel animieren

Wir können eine Keyframe-animation zu unserem .cube-Element hinzufügen.

.cube { animation: ani 2s ease-in-out infinite }

@keyframes ani {
  50% { transform: rotateY(90deg) rotateX(90deg) scale3d(.5, .5, .5) }
  100% { transform: rotateY(180deg) rotateX(180deg) }
}

Das Ergebnis ist unten zu sehen.

Animated gif. Black cube wireframe, scaling down and then back up as it rotates around its vertical axis.
Der animierte Würfel (Live-Demo).

Aktueller Support-Status und Cross-Browser-Version

Diejenigen unter Ihnen, die keinen WebKit-Browser verwenden, haben möglicherweise bemerkt, dass die obigen Demos nicht funktionieren. Dies liegt daran, dass Firefox und Edge derzeit die Verwendung von calc()-Werten nur anstelle von Längenwerten unterstützen. Dies schließt die einheitenlosen und Winkelwerte innerhalb von rotate3d() ein. Eine Möglichkeit, die Kompatibilität über Browser hinweg zu gewährleisten, besteht darin, --j nicht durch das calc(1 - var(--i))-Äquivalent zu ersetzen und eine Winkel---a-Custom-Property anstelle von calc(var(--m)*90deg) zu verwenden.

.cube__face {
  transform: rotate3d(var(--i, 0), var(--j, 1), 0, var(--a)) 
    translateZ(.5*$cube-edge);
  
  &:nth-child(n + 5) { --i: 1; --j: 0 }
  
  @for $f from 1 to 6 {
    &:nth-child(#{$f + 1}) { --a: if($f < 4, $f, pow(-1, $f))*90deg }
  }
}

Das bedeutet zwar, dass wir nun etwas Redundanz haben, aber es ist nicht so schlimm und unser Ergebnis ist jetzt Cross-Browser.

Text und Hintergründe hinzufügen

Als nächstes können wir den Würfelflächen Text hinzufügen. Entweder den gleichen für alle.

.cube
  - 6.times do
    .cube__face Boo!

... Oder einen anderen für jede (wir wechseln hier zu Pug, da es uns erlaubt, in diesem Fall etwas weniger Code als Haml zu schreiben).

- var txt = ['ginger', 'anise', 'nutmeg', 'cinnamon', 'vanilla', 'cloves'];
- var n = txt.length;

.cube
  while n--
    .cube__face #{txt[n]}

In diesem Fall setzen wir auch text-align: center, die line-height auf $cube-edge und passen $cube-edge und die font-size-Werte für die beste Textanpassung an.

$cube-edge: 5em;

.cube {
 font: 8vmin/ #{$cube-edge} cookie, cursive;
 text-align: center;
}

Wir erhalten das folgende Ergebnis.

Black cube wireframe rotated in 3D with text on every one of the cube faces.
Der Würfel mit Text (Live-Demo, animiert).

Wir könnten unseren Flächen auch einige pastellfarbene Verlaufshintergründe geben.

$pastels: (#feffaa, #b2ff90) (#fbc2eb, #a6c1ee) (#84fab0, #8fd3f4) (#a1c4fd, #c2e9fb) 
  (#f6d365, #fda085) (#ffecd2, #fcb69f);

.cube__face {
  background: linear-gradient(var(--ga), var(--gs));
  
  @for $f from 0 to 6 {
    &:nth-child(#{$i + 1}) {
      --ga: random(360)*1deg; /* gradient angle */
      --gs: nth($pastels, $f + 1); /* gradient stops */
    }
  }
}

Das Obige ergibt einen schönen pastellfarbenen Würfel.

Cube rotated in 3D with a different pastel gradient background for each of its faces.
Der Pastellwürfel (Live-Demo, animiert).

Ein Anwendungsfall

Ich habe diese Methode zur Erstellung von Quaderformen in einer Demo verwendet, die von einer Animationsschleife von Dave Whyte inspiriert ist.

Animated gif. Cuboidal bricks are falling one by one to form the uppermost circular ring on top of a structure
Erstellen Sie die Fabriken (Live-Demo, nur WebKit)

Drehen des Würfels per Drag & Drop

Danach gibt es noch eine letzte Sache zu klären: Was ist, wenn der Würfel nicht per CSS-Keyframes automatisch animiert wird, sondern stattdessen per Drag & Drop gedreht wird? Schauen wir uns an, wie wir das machen können!

Wir beginnen damit, unser .cube-Element auszuwählen und legen fest, was während der verschiedenen Phasen des Ziehens passiert. Bei mousedown/touchstart sperren wir alles für die Würfeldrehung. Das bedeutet, wir setzen ein Drag-Flag auf true und lesen die Koordinaten des Punkts, an dem dies geschieht, welche auch die Koordinaten sind, von denen aus die erste Bewegung, die von mousemove/touchmove erkannt wird, starten wird. Bei mousemove/touchmove, wenn das Drag-Flag true ist, drehen wir unseren Würfel. Bei mouseup/touchend und wieder nur, wenn das Drag-Flag wahr ist, führen wir eine Freigabeaktion aus: wir setzen das Drag-Flag wieder auf false und löschen die anfänglichen Koordinaten.

const _C = document.querySelector('.cube');

let drag = false, x0 = null, y0 = null;

/* helper function to handle both mouse and touch */
function getE(ev) { return ev.touches ? ev.touches[0] : ev };

function lock(ev) {
  let e = getE(ev);

  drag = true;
  x0 = e.clientX;
  y0 = e.clientY;
};

function rotate(ev) {
  if(drag) { /* rotation happens here */ }
};

function release(ev) {
  if(drag) {
    drag = false;
    x0 = y0 = null;
  }
};

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

addEventListener('mousemove', rotate, false);
addEventListener('touchmove', rotate, false);

addEventListener('mouseup', release, false);
addEventListener('touchend', release, false);

Jetzt müssen wir nur noch den Inhalt der rotate()-Funktion ausfüllen!

Für jede kleine Bewegung, die von den mousemove/touchmove-Listenern erfasst wird, haben wir einen Start- und einen Endpunkt. Die Koordinaten des Endpunkts (x,y) sind diejenigen, die wir bei jedem Auslösen von mousemove/touchmove über clientX und clientY lesen. Die Koordinaten des Startpunkts (x0,y0) sind entweder die gleichen wie die des Endpunkts der vorherigen kleinen Bewegung oder, wenn es keine vorherige Bewegung gab, die des Punkts, an dem mousedown/touchstart ausgelöst wurde. Das bedeutet, dass wir nach allem anderen, was wir innerhalb der rotate()-Funktion tun müssen, x0 auf x und y0 auf y setzen.

function rotate(ev) {
  if(drag) {
    let e = getE(ev), 
        x = e.clientX, y = e.clientY;
    
    /* rotation code here */
    	
    x0 = x;
    y0 = y;
  }
};

Als Nächstes berechnen wir die Koordinatendifferenzen zwischen dem End- und dem Startpunkt der aktuellen kleinen Bewegung entlang der beiden Achsen (dx und dy) sowie diagonal (d). Wenn d 0 ist, haben wir uns nicht wirklich bewegt (und vielleicht sollte nichts ausgelöst werden, aber nur für den Fall), also beenden wir die Funktion, ohne etwas anderes zu tun, nicht einmal das Setzen von x0 und y0 auf x und y – sie sind in diesem Fall sowieso gleich.

function rotate(ev) {
  if(drag) {
    let e = getE(ev), 
        x = e.clientX, y = e.clientY, 
        dx = x - x0, dy = y - y0, 
        d = Math.hypot(dx, dy);
		
    if(d) {
      /* actual rotation happens here */
      
      x0 = x;
      y0 = y;
    }
  }
};

Die Art und Weise, wie wir die Drehung per Drag & Drop handhaben, beginnend vom vorherigen Zustand, der in irgendeiner Weise transformiert sein kann, ist wie folgt: Wir koppeln eine rotate3d(), die der aktuellen kleinen Bewegung entspricht, an den berechneten transform-Wert unseres Würfels zu Beginn der aktuellen kleinen Bewegung. Das heißt, es sei denn, der berechnete transform-Wert ist none, in welchem Fall wir ihn an nichts koppeln. Wir könnten diese gesamte transform-Kette in ein Stylesheet oder als Inline-Stil schreiben oder... wir könnten wieder CSS-Variablen verwenden!

Im CSS setzen wir die transform-Eigenschaft des .cube-Elements auf eine rotate3d(var(--i), var(--j), 0, var(--a)), die an einen vorherigen Wert der Transformationskette var(--p) gekoppelt ist. Um die Dinge zu vereinfachen, halten wir die Komponente des Einheitsvektors der Rotationsachse entlang der z-Achse fest auf 0.

.cube {
  transform: rotate3d(var(--i), var(--j), 0, var(--a)) var(--p);
}

Da wir das Obige getan haben und CSS-Variablen vererbt werden, müssen wir nun explizit --i und --j für die .cube__face-Elemente auf 0 bzw. 1 setzen. Andernfalls werden die von .cube geerbten Werte angewendet, nicht die Standardwerte, die innerhalb von var() angegeben sind.

.cube__face {
  --i: 0; --j: 1;
  transform: rotate3d(var(--i), var(--j), 0, var(--a)) 
    translateZ(.5*$cube-edge);
}

Zurück zum JavaScript lesen wir den berechneten transform-Wert und setzen ihn auf die Variable --p. Der Rotationswinkel hängt von der Entfernung d zwischen dem Start- und dem Endpunkt unserer aktuellen kleinen Bewegung und einer Konstanten A ab. Wir begrenzen dieses Ergebnis auf zwei Dezimalstellen. Für eine Bewegungsrichtung nach oben, in negativer Richtung der y-Achse, drehen wir den Würfel im Uhrzeigersinn um die x-Achse. Das bedeutet, wir nehmen die --i-Komponente als -dy. Für eine Bewegungsrichtung nach rechts, in positiver Richtung der x-Achse, drehen wir den Würfel im Uhrzeigersinn um die y-Achse, was bedeutet, dass wir die --j-Komponente als dx nehmen.

const A = .2;

function rotate(ev) {
  if(drag) {
    let e = getE(ev), 
        x = e.clientX, y = e.clientY, 
        dx = x - x0, dy = y - y0, 
        d = Math.hypot(dx, dy);
		
    if(d) {
      _C.style.setProperty('--p', getComputedStyle(_C).transform.replace('none', ''));
      _C.style.setProperty('--a', `${+(A*d).toFixed(2)}deg`);
      _C.style.setProperty('--i', +(-dy).toFixed(2));
      _C.style.setProperty('--j', +(dx).toFixed(2));
      
      x0 = x;
      y0 = y;
    }
  }
};

Schließlich können wir einige beliebige Standardwerte für diese benutzerdefinierten Eigenschaften festlegen, damit die anfängliche Position unseres Würfels ihm ein etwas dreidimensionaleres Aussehen verleiht, als wenn wir ihn direkt von vorne betrachten würden.

.cube {
  transform: rotate3d(var(--i, -7), var(--j, 8), 0, var(--a, 47deg)) 
    var(--p, unquote(' '));
}

Der Wert unquote(' ') ergibt sich aus der Verwendung von Sass. Während ein leerer Raum ein perfekt gültiger Wert für eine CSS-Custom-Property in reinem CSS ist, wirft Sass einen Fehler, wenn es etwas wie var(--p, ) sieht, daher müssen wir diesen "kein Wert"-Standardwert mit unquote() einführen.

Das Ergebnis all dessen ist ein Würfel, den wir sowohl mit der Maus als auch mit der Berührung drehen können.

Sehen Sie den Pen von thebabydino (@thebabydino) auf CodePen.