Dzisiaj niemożliwe staje się możliwe! Na przykładzie logo DailyWeb przeanalizujemy tworzenie figur niemożliwych w CSS-ie.
Trójkąt Penrose’a
Na pewno wiele razy spotkaliście się z tzw. trójkątem Penrose’a, czyli jedną z najpopularniejszych figur niemożliwych, spopularyzowaną przez angielskiego matematyka Rogera Penrose’a w latach 50. XX wieku. Tworzy ona niesamowity efekt, prawda? Jak opisał ją sam Penrose: „niemożliwość w jej najczystszej postaci”.
Figury niemożliwe
Czym tak właściwie są figury niemożliwe? To przedstawienia figur 3D na płaszczyźnie, których przestrzenność jest pozorna, nie da się skonstruować ich trójwymiarowych odpowiedników. Najlepiej zilustruje to przykład. Oto rzeźba trójkąta niemożliwego stworzona przy pomocy złudzenia optycznego:
W odpowiedniej perspektywie, nasz realny obiekt układa się w trójkąt Penrose’a. Ten sam schemat wykorzystamy tworząc naszą figurę w CSS-ie.
Model 3D
Warto zacząć projekt od stworzenia modelu 3D naszej figury. Dzięki temu będziemy mogli oglądać szkic z różnych perspektyw i wybierzemy tą odpowiednią. Istnieje wiele programów i stron internetowych, w których możemy stworzyć taki model. W przypadku logo DailyWeb wystarczą same sześciany, przez wyszukiwarkę znalazłem stronę voxelbuilder.com, gdzie za pomocą prostego kreatora stworzyłem schemat figury, którą później odwzoruję.
Tworzenie chwilkę oczywiście zajmuje, trzeba zastanowić się nad odpowiednim rzutem, ilością sześcianów w liniach, odpowiednim ułożeniem… Gdy to już się wyklaruje, możemy przejść do implementacji modelu w CSS-ie.
Narzędzia
Już teraz możemy zauważyć, że w projekcie występuje wiele powtarzalności, cała figura składa się z szeregu sześcianów kolejno przesuwanych na odpowiednie pozycje. Skorzystamy zatem z preprocesorów – Hamla do html-a i SCSS-a do styli, aby nie powtarzać cały czas tego samego kodu.
Haml
Na początku musimy policzyć z ilu kostek składa się finalnie nasz model. W moim przypadku jest ich 18, a każdy sześcian składa się z 6 ścian. Potrzebujemy zatem 18 divów, które będą naszymi sześcianami, zawierających każdy po 6 divów-ścian. Następujący kod Hamla wygeneruje nam potrzebny html:
[code]
.container
– 18.times do
.box
– 6.times do
.wall
[/code]
-> [n].times powtarza dany blok kodu n razy
Całość owinąłem dodatkowo w kontener, aby móc manipulować całą figurą na raz.
SCSS
Poza tym, że nie musimy pisać aż tyle kodu, Sass umożliwia nam również zadeklarowanie kolorów i rozmiaru kostek w zmiennych. Dzięki temu ich ewentualna modyfikacja to prosta podmiana wartości w jednym miejscu.
[css]
//- appearance
$background-color: #222;
$box-color: #FFEE58;
$border-color: #000;
$border-size: 1px;
//- size
$wall-size: 6vw;
[/css]
Użycie jednostki zależnej od szerokości viewport w rozmiarze kostki gwarantuje, że cała figura zmieści się na ekranie urządzenia.
Zajmijmy się teraz kontenerem. W tym przypadku figura znajduje się na CodePenie, na pustej stronie, zatem warto ją wycentrować dla lepszej prezencji. Skorzystamy tu ze znanego triku z ustawieniem pozycji elementu na absolute i przesunięciem go od góry i od lewej o 50%, a następnie przetransformowaniu z translate(-50%, -50%). Ze względu na niesymetryczne ułożenie sześcianów musiałem jeszcze nieznacznie przesunąć całą figurę.
[css]
.container {
position: absolute;
top: calc(50% + #{2*$wall-size});
left: calc(50% – #{$wall-size});
transform: translate(-50%, -50%) rotateY(24.5deg) rotateZ(-10deg) rotateX(-22deg);
transform-style: preserve-3d;
}
[/css]
Po tym jak ustawiłem kilka pierwszych kostek, dorzuciłem także na oko odpowiednie rotate’y całej figury, żeby dopasować ją do perspektywy z modelu 3D przygotowanego wcześniej. Uwaga: konieczne jest dodanie tranform-style: preserve-3d; inaczej całość zostanie spłaszczona i nie uzyskamy pożądanego efektu.
Przejdźmy zatem do kodu sześcianu. Również jak w przypadku całej figury musimy dodać tranform-style: preserve-3d;
[css]
.wall {
position: absolute;
width: $wall-size;
height: $wall-size;
background-color: $box-color;
border: $border-size solid $border-color;
&:nth-of-type(1) {
transform: rotateX(90deg) translate3d(0,0,$wall-size / 2);
}
&:nth-of-type(2) {
transform: rotateX(90deg) translate3d(0,0,-$wall-size / 2);
}
&:nth-of-type(3) {
transform: translate3d(0,0,$wall-size / 2);
}
&:nth-of-type(4) {
transform: translate3d(0,0,-$wall-size / 2);
}
&:nth-of-type(5) {
transform: rotateY(90deg) translate3d(0,0,-$wall-size / 2);
}
&:nth-of-type(6) {
transform: rotateY(90deg) translate3d(0,0,$wall-size / 2);
}
}
[/css]
Ściany muszą mieć oczywiście szerokość i wysokość, taką samą z racji, że mają być kwadratami. Dodatkowy border pozwala odróżnić ściany i kostki od siebie, a samo ułożenie ścian w sześcian to po prostu wybieranie konkretnej ściany selektorem :nth-child() i ułożenie w odpowiedniej pozycji za pomocą rotacji i przesunięcia.
I właściwie większość pracy za nami. Jak dotąd mamy już sześcian i całą figurę, tyle że wszystkie kostki są w jednym miejscu – trzeba je poprzesuwać. Na modelu 3D widać, że figura składa się z czterech linii sześcianów, kolejno ułożonych za sobą, zatem możemy skorzystać z pętli dostępnych w Sassie, żeby zrobić to szybko i wygodnie. Początek następnej linii jest końcem poprzedniej, zatem wiemy jaka ma być pozycja pierwszego klocka i wystarczy następne przesuwać o długość $wall-size, czyli boku sześcianu.
[css]
.box {
@for $i from 1 through 6 {
&:nth-child(#{$i+1}) {
transform: translateY(-$i*$wall-size);
}
}
@for $i from 1 through 6 {
&:nth-child(#{$i+7}) {
transform: translateY(-6*$wall-size) translateZ($i*$wall-size);
}
}
@for $i from 1 through 3 {
&:nth-child(#{$i+13}) {
transform: translateY(($i – 6)*$wall-size) translateZ(6*$wall-size);
}
}
@for $i from 1 through 2 {
&:nth-child(#{$i+16}) {
transform: translateX(-$i*$wall-size) translateY(-3*$wall-size) translateZ(6*$wall-size);
}
}
}
[/css]
Pozostała jeszcze jedna rzecz do zrobienia – tak jak w rzeźbie, którą oglądaliśmy wcześniej, jeden klocek trzeba troszkę „przystrzyc”, żeby w naszej perspektywie pasował do figury i tworzył ten efekt niemożliwego, którego poszukujemy. Musimy zatem niektóre ściany ukryć poprzez display: none, a górną przyciąć do trapezu – własnością clip-path. Wygląd ostatniego klocka definiujemy zatem tak:
[css]
.box:nth-child(18) {
.wall {
&:nth-of-type(1) {
clip-path: polygon(50% 0%, 100% 0, 100% 100%, 0% 100%);
}
&:nth-of-type(2),
&:nth-of-type(4), :nth-of-type(5) {
display: none;
}
}
}
[/css]
Na koniec, możemy dorzucić obrót całej figury, aby pokazać wykorzystaną sztuczkę ze złudzeniem optycznym, ale to już efekt dodatkowy. Tyle wystarczyło, by stworzyć coś niemożliwego, ale jednak było to możliwe i do tego nie tak bardzo skomplikowane!
Demo tutaj:
Źródła:
https://pl.wikipedia.org/wiki/Trójkąt_Penrose’a
https://codepen.io/MyXoToD/pen/GprLZx