Sehr oft, wenn ich Switch-Variablen verwende (eine Variable, die entweder 0 oder 1 ist, ein Konzept, das in diesem Beitrag detaillierter erläutert wird), wünschte ich, ich könnte logische Operationen damit durchführen. Wir haben in CSS keine Funktionen wie not(var(--i)) oder and(var(--i), var(--k)), aber wir können diese und mehr mit arithmetischen Operationen in einer calc()-Funktion emulieren.
Dieser Artikel zeigt Ihnen, welche calc()-Formeln wir für jede logische Operation verwenden müssen, und erklärt, wie und warum sie mit einigen Anwendungsfällen verwendet werden, die zu diesem Artikel geführt haben.
Wie: die Formeln
not
Dies ist ziemlich einfach: Wir subtrahieren die Switch-Variable (nennen wir sie --j) von 1.
--notj: calc(1 - var(--j))
Wenn --j 0 ist, dann ist --notj 1 (1 - 0). Wenn j 1 ist, dann ist --notj 0 (1 - 1).
and
Wenn Sie jemals Elektronikkurse belegt haben (insbesondere etwas wie Programmierbare Logiksysteme oder Integrierte Schaltkreise), dann wissen Sie bereits, welche Formel wir hier verwenden müssen. Aber springen wir nicht direkt ins kalte Wasser.
Das and zweier Operanden ist wahr, wenn und nur wenn beide wahr sind. Die beiden Operanden in unserem Fall sind zwei Switch-Variablen (nennen wir sie --k und --i). Jede von ihnen kann unabhängig von der anderen entweder 0 oder 1 sein. Das bedeutet, wir können uns in einem von vier möglichen Szenarien befinden:
--k: 0,--i: 0--k: 0,--i: 1--k: 1,--i: 0--k: 1,--i: 1
Das Ergebnis der and-Operation ist 1, wenn beide unserer Switch-Variablen 1 sind, und sonst 0. Anders betrachtet ist dieses Ergebnis 0, wenn mindestens eine der beiden Switch-Variablen 0 ist.
Nun müssen Sie es sich so vorstellen: Das Ergebnis welcher arithmetischen Operation ist 0, wenn mindestens einer der beiden Operanden 0 ist? Das ist die Multiplikation, da die Multiplikation von irgendetwas mit 0 uns 0 ergibt!
Unsere --and-Formel lautet also:
--and: calc(var(--k)*var(--i))
Betrachten wir jedes unserer vier möglichen Szenarien:
- für
--k: 0,--i: 0, ist--and0(0*0) - für
--k: 0,--i: 1, ist--and0(0*1) - für
--k: 1,--i: 0, ist--and0(1*0) - für
--k: 1,--i: 1, ist--and1(1*1)
nand
Da nand not and ist, müssen wir --j in der not-Formel durch die Formel für and ersetzen.
--nand: calc(1 - var(--k)*var(--i))
Für jedes unserer vier möglichen Szenarien erhalten wir:
- für
--k: 0,--i: 0, ist--nand1(1 - 0*0 = 1 - 0) - für
--k: 0,--i: 1, ist--nand1(1 - 0*1 = 1 - 0) - für
--k: 1,--i: 0, ist--nand1(1 - 1*0 = 1 - 0) - für
--k: 1,--i: 1, ist--nand0(1 - 1*1 = 1 - 1)
or
Das Ergebnis der or-Operation ist 1, wenn mindestens eine unserer Switch-Variablen 1 ist, und sonst 0 (wenn beide 0 sind).
Der erste Instinkt hier ist, zur Addition zu greifen, aber obwohl diese uns 0 ergibt, wenn beide --k und --i 0 sind, und 1, wenn eine 0 und die andere 1 ist, ergibt sie uns 2, wenn beide 1 sind. Das funktioniert also nicht wirklich.
Aber wir können die altbewährten De Morgan’schen Regeln verwenden, von denen eine besagt:
not (A or B) = (not A) and (not B)
Das bedeutet, das Ergebnis der or-Operation ist die Negation der and-Operation zwischen den Negationen von --k und --i. Dies in CSS eingesetzt ergibt:
--or: calc(1 - (1 - var(--k))*(1 - var(--i)))
Für jedes Szenario erhalten wir:
- für
--k: 0,--i: 0, ist--or0(1 - (1 - 0)*(1 - 0) = 1 - 1*1 = 1 - 1) - für
--k: 0,--i: 1, ist--or1(1 - (1 - 0)*(1 - 1) = 1 - 1*0 = 1 - 0) - für
--k: 1,--i: 0, ist--or1(1 - (1 - 1)*(1 - 0) = 1 - 0*1 = 1 - 0) - für
--k: 1,--i: 1, ist--or1(1 - (1 - 1)*(1 - 1) = 1 - 0*0 = 1 - 0)
nor
Da nor not or ist, haben wir:
--nor: calc((1 - var(--k))*(1 - var(--i)))
Für jedes unserer vier möglichen Szenarien erhalten wir:
- für
--k: 0,--i: 0, ist--nor1((1 - 0)*(1 - 0) = 1*1) - für
--k: 0,--i: 1, ist--nor0((1 - 0)*(1 - 1) = 1*0) - für
--k: 1,--i: 0, ist--nor0((1 - 1)*(1 - 0) = 0*1) - für
--k: 1,--i: 1, ist--nor0((1 - 1)*(1 - 1) = 0*0)
xor
Das Ergebnis der xor-Operation ist 1, wenn einer der beiden Operanden 1 und der andere 0 ist. Das erscheint zunächst kniffliger, aber wenn wir bedenken, dass dies bedeutet, dass die beiden Operanden unterschiedlich sein müssen, damit das Ergebnis 1 ist (sonst ist es 0), stoßen wir auf die richtige arithmetische Operation, die wir in calc() verwenden können: die Subtraktion!
Wenn --k und --i gleich sind, ergibt die Subtraktion von --i von --k 0. Andernfalls, wenn wir --k: 0, --i: 1 haben, ist das Ergebnis derselben Subtraktion -1; wenn wir --k: 1, --i: 0 haben, ist das Ergebnis 1.
Nah dran, aber nicht ganz! Wir erhalten das gewünschte Ergebnis in drei von vier Szenarien, aber wir müssen in dem Szenario --k: 0, --i: 1 1 und nicht -1 erhalten.
Eines haben -1, 0 und 1 jedoch gemeinsam: Wenn man sie mit sich selbst multipliziert, erhält man ihren Absolutwert (der für -1 und 1 jeweils 1 ist). Die tatsächliche Lösung besteht also darin, diese Differenz mit sich selbst zu multiplizieren.
--xor: calc((var(--k) - var(--i))*(var(--k) - var(--i)))
Testen wir jedes unserer vier möglichen Szenarien:
- für
--k: 0,--i: 0, ist--xor0((0 - 0)*(0 - 0) = 0*0) - für
--k: 0,--i: 1, ist--xor1((0 - 1)*(0 - 1) = -1*-1) - für
--k: 1,--i: 0, ist--xor1((1 - 0)*(1 - 0) = 1*1) - für
--k: 1,--i: 1, ist--xor0((1 - 1)*(1 - 1) = 0*0)
Warum: Anwendungsfälle
Sehen wir uns ein paar Beispiele an, die logische Operationen in CSS nutzen. Beachten Sie, dass ich andere Aspekte dieser Demos nicht im Detail erläutere, da sie außerhalb des Rahmens dieses speziellen Artikels liegen.
Deaktiviertes Panel nur auf kleinen Bildschirmen ausblenden
Dies ist ein Anwendungsfall, auf den ich gestoßen bin, als ich an einer interaktiven Demo gearbeitet habe, die es Benutzern ermöglicht, verschiedene Parameter zu steuern, um ein visuelles Ergebnis zu ändern. Für erfahrenere Benutzer gibt es auch ein Panel mit erweiterten Steuerelementen, das standardmäßig deaktiviert ist. Es kann jedoch aktiviert werden, um manuell noch mehr Parameter steuern zu können.
Da diese Demo responsiv sein soll, ändert sich das Layout mit dem Viewport. Wir möchten auch vermeiden, dass Dinge auf kleineren Bildschirmen überladen werden, wenn wir es vermeiden können. Daher macht es keinen Sinn, die erweiterten Steuerelemente anzuzeigen, wenn sie deaktiviert sind und wir uns im schmalen Bildschirmfall befinden.
Die folgende Collage von Screenshots zeigt die Ergebnisse für jedes der vier möglichen Szenarien.

Schauen wir uns also an, was das in Bezug auf CSS bedeutet!
Zunächst verwenden wir auf dem <body> einen Schalter, der im schmalen Bildschirmfall von 0 auf 1 im breiten Bildschirmfall wechselt. Wir ändern auch die flex-direction auf diese Weise (wenn Sie eine detailliertere Erklärung wünschen, wie das funktioniert, lesen Sie meinen zweiten Artikel über DRY-Switching mit CSS-Variablen).
body {
--k: var(--wide, 0);
display: flex;
flex-direction: var(--wide, column);
@media (orientation: landscape) { --wide: 1 }
}
Dann haben wir einen zweiten Schalter auf dem Panel für erweiterte Steuerelemente. Dieser zweite Schalter ist 0, wenn die Checkbox nicht angekreuzt ist, und 1, wenn die Checkbox :checked ist. Mit Hilfe dieses Schalters geben wir unserem Panel für erweiterte Steuerelemente einen deaktivierten Look (über eine filter-Kette) und deaktivieren es auch (über pointer-events). Hier ist not sehr nützlich, da wir den Kontrast und die Deckkraft im deaktivierten Fall verringern möchten.
.advanced {
--i: var(--enabled, 0);
--noti: calc(1 - var(--i));
filter:
contrast(calc(1 - var(--noti)*.9))
opacity(calc(1 - var(--noti)*.7));
pointer-events: var(--enabled, none);
[id='toggle']:checked ~ & { --enabled: 1 }
}
Wir möchten, dass das Panel für erweiterte Steuerelemente erweitert bleibt, wenn wir uns im breiten Bildschirmfall befinden (also wenn --k 1 ist), unabhängig davon, ob die Checkbox :checked ist oder nicht, *oder* wenn die Checkbox :checked ist (also wenn --i 1 ist), unabhängig davon, ob wir uns im breiten Bildschirmfall befinden oder nicht.
Das ist genau die or-Operation!
Wir berechnen also eine --or-Variable.
.advanced {
/* same as before */
--or: calc(1 - (1 - var(--k))*(1 - var(--i)));
}
Wenn diese --or-Variable 0 ist, bedeutet dies, dass wir uns im schmalen Bildschirmfall befinden *und* unsere Checkbox nicht angekreuzt ist. Daher möchten wir die height des Panels für erweiterte Steuerelemente und auch seinen vertikalen margin auf 0 setzen.
.advanced {
/* same as before */
margin: calc(var(--or)*#{$mv}) 0;
height: calc(var(--or)*#{$h});
}
Das ergibt das gewünschte Ergebnis (Live-Demo).
Verwenden Sie dieselben Formeln, um mehrere Flächen einer 3D-Form zu positionieren
Dies ist ein Anwendungsfall, auf den ich gestoßen bin, als ich im Sommer an meinem persönlichen Projekt arbeitete, die Johnson-Polyeder mit CSS zu erstellen.
Betrachten wir eine dieser Formen, zum Beispiel die gyrolängliche pentagonale Rotunde (J25), um zu sehen, wie logische Operationen hier nützlich sind.
Diese Form besteht aus einer pentagonalen Rotunde ohne die große zehnseitige Basis und einem zehnseitigen Antiprisma ohne seine obere Zehneck-Basis. Die interaktive Demo unten zeigt, wie diese beiden Komponenten durch das Falten ihrer Netze von Flächen in 3D aufgebaut und dann verbunden werden, um die gewünschte Form zu erhalten.
Siehe das Pen von thebabydino (@thebabydino) auf CodePen.
Wie oben zu sehen ist, sind die Flächen entweder Teil des Antiprismas oder Teil der Rotunde. Hier führen wir unsere erste Switch-Variable --i ein. Diese ist 0 für die Flächen, die Teil des Antiprismas sind, und 1 für die Flächen, die Teil der Rotunde sind. Die Antiprismasflächen haben eine Klasse .mid, da wir eine weitere Rotunde an der anderen Antiprismasbasis hinzufügen können und das Antiprisma dann in der Mitte wäre. Die Rotundasflächen haben eine Klasse .cup, da dieser Teil wie eine Kaffeetasse aussieht... ohne Henkel!
.mid { --i: 0 }
.cup { --i: 1 }
Wenn wir uns nur auf die Seitenflächen konzentrieren, können diese eine nach oben oder unten zeigende Ecke haben. Hier führen wir unsere zweite Variable --k ein. Diese ist 0, wenn sie eine nach oben zeigende Ecke haben (solche Flächen haben die Klasse .dir) und 1, wenn sie umgekehrt sind und eine nach unten zeigende Ecke haben (diese Flächen haben die Klasse .rev).
.dir { --k: 0 }
.rev { --k: 1 }
Das Antiprisma hat 10 Seitenflächen (alle Dreiecke), die nach oben zeigen, jede verbunden mit einer Kante seiner zehnseitigen Basis, die auch eine Basis für die zusammengesetzte Form ist. Es hat auch 10 Seitenflächen (ebenfalls alle Dreiecke), die nach unten zeigen, jede verbunden mit einer Kante seiner anderen zehnseitigen Basis (derjenigen, die auch die zehnseitige Basis der Rotunde ist und daher keine Basis für die zusammengesetzte Form ist).
Die Rotunde hat 10 Seitenflächen, die nach oben zeigen, abwechselnd Dreiecke und Fünfecke, jede verbunden mit der zehnseitigen Basis, die auch eine Basis für das Antiprisma ist (also keine Basis für die zusammengesetzte Form ist). Sie hat auch 5 Seitenflächen, alle Dreiecke, die nach unten zeigen, jede verbunden mit einer Kante ihrer pentagonalen Basis.
Die interaktive Demo unten ermöglicht es uns, jede dieser vier Flächengruppen besser zu sehen, indem wir nur eine auf einmal hervorheben. Sie können die Pfeile unten verwenden, um auszuwählen, welche Flächengruppe hervorgehoben wird. Sie können auch die Rotation um die y-Achse aktivieren und die Neigung der Form ändern.
Siehe das Pen von thebabydino (@thebabydino) auf CodePen.
Wie bereits erwähnt, können die Seitenflächen entweder Dreiecke oder Fünfecke sein.
.s3gon { --p: 0 }
.s5gon { --p: 1 }
Da alle ihre Seitenflächen (.lat) sowohl des Antiprismas als auch der Rotunde eine Kante mit einer der beiden Basisflächen jeder Form gemeinsam haben, nennen wir diese gemeinsamen Kanten die Basis kanten der Seitenflächen.
Die interaktive Demo unten hebt diese Kanten, ihre Endpunkte und ihre Mittelpunkte hervor und ermöglicht es, die Formen aus verschiedenen Blickwinkeln zu betrachten, dank der Auto-Rotationen um die y-Achse, die jederzeit gestartet/pausiert werden können, und der manuellen Rotationen um die x-Achse, die über die Schieberegler gesteuert werden können.
Siehe das Pen von thebabydino (@thebabydino) auf CodePen.
Um es uns einfacher zu machen, setzen wir den transform-origin der .lat-Flächen auf die Mitte ihrer Basis kanten (untere horizontale Kanten).
Wir stellen auch sicher, dass wir diese Flächen so positionieren, dass diese Mittelpunkte genau in der Mitte des Szenenelements liegen, das unsere gesamte 3D-Form enthält.
Wenn der transform-origin mit dem Mittelpunkt der Basis kante übereinstimmt, bedeutet dies, dass jede Rotation, die wir auf einer Fläche durchführen, um den Mittelpunkt ihrer Basis kante stattfindet, wie in der interaktiven Demo unten veranschaulicht.
Siehe das Pen von thebabydino (@thebabydino) auf CodePen.
Wir platzieren unsere Seitenflächen dort, wo wir sie haben wollen, in vier Schritten:
- Wir rotieren sie um ihre y-Achse, sodass ihre Basis kanten parallel zu ihren endgültigen Positionen sind. (Dies rotiert auch ihr lokales Koordinatensystem – die z-Achse eines Elements zeigt immer in die Richtung, in die dieses Element zeigt.)
- Wir übersetzen sie, sodass ihre Basis kanten mit ihren endgültigen Positionen übereinstimmen (entlang der Kanten der Basisflächen der beiden Komponenten).
- Wenn sie eine nach unten zeigende Ecke haben müssen, rotieren wir sie um ihre z-Achse um eine halbe Drehung.
- Wir rotieren sie um ihre x-Achse in ihre endgültigen Positionen.
Diese Schritte werden in der interaktiven Demo unten veranschaulicht, wo Sie sie durchgehen können und auch die gesamte Form rotieren können (mithilfe der Play/Pause-Schaltfläche für die Rotation um die y-Achse und dem Schieberegler für die Rotation um die x-Achse).
Siehe das Pen von thebabydino (@thebabydino) auf CodePen.
Der Wert der Rotation um die y-Achse basiert hauptsächlich auf den Flächenindizes und weniger auf unseren Switch-Variablen, obwohl er auch von diesen abhängt.
Die Struktur ist wie folgt:
- var n = 5; //- number of edges/ vertices of small base
section.scene
//- 3D shape element
.s3d
//- the faces, each a 2D shape element (.s2d)
//- lateral (.lat) antiprism (.mid) faces,
//- first half pointing up (.dir), others pointing down (.rev)
//- all of them being triangles (.s3gon)
- for(var j = 0; j < 4*n; j++)
.s2d.mid.lat.s3gon(class=j < 2*n ? 'dir' : 'rev')
//- lateral (.lat) rotunda (.cup) faces that point up (.dir),
//- both triangles (.s3gon) and pentagons (.s5gon)
- for(var j = 0; j < n; j++)
.s2d.cup.lat.s3gon.dir
.s2d.cup.lat.s5gon.dir
//- lateral (.lat) rotunda (.cup) faces that point down (.rev)
//- all of them triangles (.s3gon)
- for(var j = 0; j < n; j++)
.s2d.cup.lat.s3gon.rev
//- base faces,
//- one for the antiprism (.mid),
//- the other for the rotunda (.cup)
.s2d.mid.base(class=`s${2*n}gon`)
.s2d.cup.base(class=`s${n}gon`)
Was uns den folgenden HTML-Code ergibt:
<section class="scene">
<div class="s3d">
<!-- LATERAL faces -->
<div class="s2d mid lat s3gon dir"></div>
<!-- 9 more identical faces,
so we have 10 lateral antiprism faces pointing up -->
<div class="s2d mid lat s3gon rev"></div>
<!-- 9 more identical faces,
so we have 10 lateral antiprism faces pointing down -->
<div class="s2d cup lat s3gon dir"></div>
<div class="s2d cup lat s5gon dir"></div>
<!-- 4 more identical pairs,
so we have 10 lateral rotunda faces pointing up -->
<div class="s2d cup lat s3gon rev"></div>
<!-- 4 more identical faces,
so we have 5 lateral rotunda faces pointing down -->
<!-- BASE faces -->
<div class="s2d mid base s10gon"></div>
<div class="s2d cup base s5gon"></div>
</div>
</section>
Dies bedeutet, dass die Flächen 0... 9 die 10 seitlichen Antiprismasflächen sind, die nach oben zeigen, die Flächen 10... 19 sind die 10 seitlichen Antiprismasflächen, die nach unten zeigen, die Flächen 20... 29 sind die 10 seitlichen Rotundasflächen, die nach oben zeigen, und die Flächen 30... 34 sind die 5 seitlichen Rotundasflächen, die nach unten zeigen.
Was wir hier also tun, ist, einen Index --idx für die Seitenflächen festzulegen.
$n: 5; // number of edges/ vertices of small base
.lat {
@for $i from 0 to 2*$n {
&:nth-child(#{2*$n}n + #{$i + 1}) { --idx: #{$i} }
}
}
Dieser Index beginnt bei 0 für jede Flächengruppe, was bedeutet, dass die Indizes für die Flächen 0... 9, 10... 19 und 20... 29 von 0 bis 9 reichen, während die Indizes für die Flächen 30... 34 von 0 bis 4 reichen. Großartig, aber wenn wir diese Indizes einfach mit dem Basiswinkel1 des gemeinsamen Zehnecks multiplizieren, um die y-Achsenrotation zu erhalten, die wir in diesem Schritt benötigen,
--ay: calc(var(--idx)*#{$ba10gon});
transform: rotatey(var(--ay))
…dann erhalten wir das folgende Endergebnis. Ich zeige hier das Endergebnis, da es schwierig ist, zu sehen, was falsch ist, wenn man sich das Zwischenergebnis ansieht, das nach Anwendung der Rotation um die y-Achse erhalten wird.
Siehe das Pen von thebabydino (@thebabydino) auf CodePen.
Das ist... nicht ganz das, was wir erreichen wollten!
Sehen wir uns also die Probleme des obigen Ergebnisses an und wie wir sie mit Hilfe unserer Switch-Variablen und booleschen Operationen darauf lösen können.
Das erste Problem ist, dass die seitlichen Antiprismasflächen, die nach oben zeigen, um die Hälfte des Basiswinkels eines regelmäßigen Zehnecks versetzt werden müssen. Das bedeutet, dass .5 zu --idx addiert oder subtrahiert wird, bevor es mit dem Basiswinkel multipliziert wird, *aber nur für diese Flächen*.
Siehe das Pen von thebabydino (@thebabydino) auf CodePen.
Die Flächen, die wir ansprechen wollen, sind diejenigen, für die sowohl --i als auch --k 0 sind. Was wir hier brauchen, ist also, das Ergebnis ihrer nor-Operation mit .5 zu multiplizieren.
--nor: calc((1 - var(--k))*(1 - var(--i)));
--j: calc(var(--idx) + var(--nor)*.5);
--ay: calc(var(--j)*#{$ba10gon});
transform: rotatey(var(--ay));
Das zweite Problem ist, dass die seitlichen Rotundasflächen, die nach unten zeigen, nicht so verteilt sind, wie sie sein sollten, so dass jede von ihnen eine gemeinsame Basis kante mit dem Basis Fünfeck hat und die zur Basis gegenüberliegende Ecke mit den dreieckigen Rotundasflächen, die nach oben zeigen, gemeinsam hat. Das bedeutet, dass --idx mit 2 multipliziert werden muss, aber nur für diese Flächen.
Siehe das Pen von thebabydino (@thebabydino) auf CodePen.
Was wir jetzt ansprechen, sind die Flächen, für die sowohl --i als auch --k 1 sind (also die Flächen, für die das Ergebnis der and-Operation 1 ist). Wir müssen also --idx mit 1 plus ihrem and multiplizieren.
--and: calc(var(--k)*var(--i));
--nor: calc((1 - var(--k))*(1 - var(--i)));
--j: calc((1 + var(--and))*var(--idx) + var(--nor)*.5);
--ay: calc(var(--j)*#{$ba10gon});
transform: rotatey(var(--ay));
Der nächste Schritt ist die Translation, für die wir translate3d() verwenden. Wir bewegen keine unserer Flächen nach links oder rechts, daher ist der Wert entlang der x-Achse immer 0. Wir bewegen sie jedoch vertikal (entlang der y-Achse) und vorwärts (entlang der z-Achse).
Vertikal wollen wir, dass die Tassenflächen, die später nach unten gedreht werden, ihre Basis kante in der Ebene der kleinen (pentagonalen) Basis der Tasse (und der zusammengesetzten Form) haben. Das bedeutet, dass die Flächen, für die --i 1 und --k 1 ist, nach oben (negative Richtung) um die Hälfte der Gesamthöhe der zusammengesetzten Form (eine Gesamthöhe, die wir als $h berechnet haben) bewegt werden. Wir brauchen also hier die and-Operation.
// same as before
--and: calc(var(--i)*var(--k));
--y: calc(var(--and)*#{-.5*$h});
transform: rotatey(var(--ay))
translate3d(0, var(--y, 0), var(--z, 0));
Wir möchten auch, dass alle anderen Tassenflächen sowie die Antiprismasflächen, die schließlich nach unten zeigen, ihre Basis kante in der gemeinsamen Ebene zwischen der Tasse und dem Antiprisma haben. Das bedeutet, dass die Flächen, für die --i 1 und --k 0 ist, sowie die Flächen, für die --i 0 und --k 1 ist, nach unten (positive Richtung) um die Hälfte der Höhe der zusammengesetzten Form und dann wieder nach oben (negative Richtung) um die Höhe des Antiprismas ($h-mid) verschoben werden. Und was Sie wissen müssen, das ist die xor-Operation!
// same as before
--xor: calc((var(--k) - var(--i))*(var(--k) - var(--i)));
--and: calc(var(--i)*var(--k));
--y: calc(var(--xor)*#{.5*$h - $h-mid} -
var(--and)*#{.5*$h});
transform: rotatey(var(--ay))
translate3d(0, var(--y, 0), var(--z, 0));
Schließlich möchten wir, dass die Antiprismasflächen, die nach oben zeigen, in der unteren Basis ebene der zusammengesetzten Form (und des Antiprismas) liegen. Das bedeutet, dass die Flächen, für die --i 0 und --k 0 ist, nach unten (positive Richtung) um die Hälfte der Gesamthöhe der zusammengesetzten Form verschoben werden. Wir verwenden also hier die nor-Operation!
// same as before
--nor: calc((1 - var(--k))*(1 - var(--i)));
--xor: calc((var(--k) - var(--i))*(var(--k) - var(--i)));
--and: calc(var(--i)*var(--k));
--y: calc(var(--nor)*#{.5*$h} +
var(--xor)*#{.5*$h - $h-mid} -
var(--and)*#{.5*$h});
transform: rotatey(var(--ay))
translate3d(0, var(--y, 0), var(--z, 0));
Siehe das Pen von thebabydino (@thebabydino) auf CodePen.
Entlang der z-Richtung wollen wir die Flächen so bewegen, dass ihre Basis kanten mit den Kanten der Basisflächen der zusammengesetzten Form oder den Kanten der gemeinsamen Basis (die keine Fläche der zusammengesetzten Form ist) übereinstimmen, die von den beiden 3D-Komponenten geteilt wird. Für die oberen Flächen der Tasse (die wir später nach unten drehen) ist die Platzierung an den Kanten eines Fünfecks, während für alle anderen Flächen der zusammengesetzten Form die Platzierung an den Kanten eines Zehnecks erfolgt.
Das bedeutet, die Flächen, für die --i 1 und --k 1 ist, werden um den Inradius der pentagonalen Basis nach vorne verschoben, während alle anderen Flächen um den Inradius eines zehnseitigen Kreises nach vorne verschoben werden. Die benötigten Operationen sind also and und nand!
// same as before
--and: calc(var(--i)*var(--k));
--nand: calc(1 - var(--and));
--z: calc(var(--and)*#{$ri5gon} + var(--nand)*#{$ri10gon});
transform: rotatey(var(--ay))
translate3d(0, var(--y, 0), var(--z, 0));
Siehe das Pen von thebabydino (@thebabydino) auf CodePen.
Als nächstes möchten wir alle .rev-Flächen (für die --k 1 ist) nach unten zeigen lassen. Das ist ziemlich einfach und erfordert keine logische Operation. Wir müssen nur eine halbe Umdrehung um die z-Achse zur Transformationskette hinzufügen, aber nur für die Flächen, für die --k 1 ist.
// same as before
--az: calc(var(--k)*.5turn);
transform: rotatey(var(--ay))
translate3d(0, var(--y), var(--z))
rotate(var(--az));
Siehe das Pen von thebabydino (@thebabydino) auf CodePen.
Die pentagonalen Flächen (für die --p 1 ist) werden dann alle um die x-Achse um einen bestimmten Winkel gedreht.
--ax: calc(var(--p)*#{$ax5});
Im Fall der dreieckigen Flächen (für die --p 0 ist, was bedeutet, dass wir --notp verwenden müssen) haben wir einen bestimmten Drehwinkel für die Flächen des Antiprismas ($ax3-mid), einen anderen Winkel für die Flächen der Rotunde, die nach oben zeigen ($ax3-cup-dir), und einen weiteren Winkel für die Rotundasflächen, die nach unten zeigen ($ax3-cup-red).
Die Antiprismasflächen sind diejenigen, für die --i 0 ist, daher müssen wir ihren entsprechenden Winkelwert hier mit --noti multiplizieren. Die Rotundasflächen sind diejenigen, für die --i 1 ist, und von diesen zeigen diejenigen nach oben, für die --k 0 ist, und diejenigen, die nach unten zeigen, sind diejenigen, für die --k 1 ist.
--notk: calc(1 - var(--k));
--noti: calc(1 - var(--i));
--notp: calc(1 - var(--p));
--ax: calc(var(--notp)*(var(--noti)*#{$ax3-mid} +
var(--i)*(var(--notk)*#{$ax3-cup-dir} + var(--k)*#{$ax3-cup-rev})) +
var(--p)*#{$ax5});
transform: rotatey(var(--ay))
translate3d(0, var(--y), var(--z))
rotate(var(--az))
rotatex(var(--ax));
Das ergibt das Endergebnis!
Siehe das Pen von thebabydino (@thebabydino) auf CodePen.
1 Für jedes regelmäßige Polygon (wie z.B. jede der Flächen unserer Formen) ist der Bogen, der einer Kante entspricht, sowie der Winkel zwischen den Umkreisradien zu den Enden dieser Kante (unser Basiswinkel) ein voller Kreis (360°) geteilt durch die Anzahl der Kanten. Bei einem gleichseitigen Dreieck beträgt der Winkel 360°/3 = 120°. Für ein regelmäßiges Fünfeck beträgt der Winkel 360°/5 = 72°. Für ein regelmäßiges Zehneck beträgt der Winkel 360°/10 = 36°. ↪️
Siehe das Pen von thebabydino (@thebabydino) auf CodePen.
Erstaunliche Arbeit, Ana!
Ich habe vor etwa einem Jahr mit bedingten Anweisungen und Logikgattern in CSS experimentiert und sogar Geoff die Idee für einen Artikel vorgeschlagen, aber ich konnte keine praktische Anwendung dafür finden, so dass sie in meinem Backlog verloren ging.
Ich wusste, wenn es jemanden auf der Welt gäbe, der das gut nutzen könnte, dann müssten Sie es sein!
Ich habe auch ein XNOR-Gatter und einige „kleiner / gleich / größer“-Vergleiche, falls jemand neugierig ist, aber meine Formeln sind nur zusammengebastelt und nicht wirklich durchdacht wie Ihre.
Phänomenale Arbeit, Ana. Danke, dass Sie das alles geteilt haben.
M-ai dat pe spate, Ana! Erstaunlich.
Da Sie nur bitbasierte Berechnungen mit 1 oder 0 verwenden, Ihre Darstellung von –or
calc(1 – (1 – var(–k))*(1 – var(–i)))
kann vereinfacht werden zu
calc(var(–k) + var(–i) – (var(–k) * var(–i)))
Mit anderen Worten: k + i – ki