Snart er det JavaZone, og da blir det kosetime med Christian og meg. Vi skal lage et nytt zombiespill - denne gangen med terninger.
Terningspill er langt morsommere hvis man kan se terningene rulle, så jeg brettet opp ermene (ikke armene) og skrev litt CSS i forberedelse til presentasjonen. Her er det jeg lærte om å kaste terninger med CSS.
¶Først trenger vi en kube
En kube har seks sider, og de må vi tegne hver for seg:
<div class="example">
<div class="cube">
<div class="face face-1"></div>
<div class="face face-2"></div>
<div class="face face-3"></div>
<div class="face face-4"></div>
<div class="face face-5"></div>
<div class="face face-6"></div>
</div>
</div>
La oss forankre sidene i en kube:
.cube {
width: 120px;
height: 120px;
position: relative;
transform-style: preserve-3d;
display: inline-block;
}
Her sier vi position: relative
fordi alle sidene av terningen skal ligge oppå
hverandre i utgangspunktet.
Det neste viktige poenget er preserve-3d
: Dette lar oss rotere dette elementet
og dets barn i samme tredimensjonale kontekst. Dette kommer vi tilbake til snart.
På tide å legge litt styling på sidene:
.face {
height: 120px;
width: 120px;
background-color: rgba(255,255,255,0.7);
position: absolute;
border-radius: 6px;
border: 1px solid #aaa;
box-shadow: inset 0 0 20px rgba(0,0,0,0.2);
}
På dette tidspunktet ser terningen vår slik ut:
Ikke så tredimensjonal enda. Litt vanskelig å se at det er seks sider. For å kunne se hva som skjer videre, må vi først rotere kuben litt:
.cube {
transform: rotateY(30deg) rotateX(30deg) rotateZ(30deg);
}
La oss starte litt naivt, og bare rotere all sidene på plass:
.face-1 { }
.face-2 { transform: rotateY(90deg); }
.face-3 { transform: rotateY(90deg) rotateX(90deg); }
.face-4 { transform: rotateY(180deg) rotateZ(90deg); }
.face-5 { transform: rotateY(-90deg) rotateZ(90deg); }
.face-6 { transform: rotateX(-90deg); }
Observer at alle sidene har rotert om den sentrale aksen i kuben. Det ser ikke ut som noen terning akkurat.
Vi trenger å skyve hver side vekk fra midten. Hvor mye? Sidene våre er jo
120px
store, så vi må skyve 60px
i hver retning. Voila:
.face-1 { transform: translateZ(60px); }
.face-2 { transform: rotateY(90deg) translateZ(60px); }
.face-3 { transform: rotateY(90deg) rotateX(90deg) translateZ(60px); }
.face-4 { transform: rotateY(180deg) rotateZ(90deg) translateZ(60px); }
.face-5 { transform: rotateY(-90deg) rotateZ(90deg) translateZ(60px); }
.face-6 { transform: rotateX(-90deg) translateZ(60px); }
Og dermed har vi fått en kube. Men den ser ikke helt … riktig ut. Det er noe galt med perspektivet:
.example {
perspective: 400px;
}
Ahh, det var bedre.
Når man setter perspective
så sier man hvor langt det er mellom brukeren og
punkter i posisjon Z0. Punkter med høyere Z oppleves nærmere, og punkter med
lavere Z lenger unna. Forsvinningspunktet er per default i midten av elementet
med perspektiv, men dette kan også flyttes.
La oss prøve det. Først fjerner vi roteringen av kuben for å gjøre det tydeligere:
Nå kan vi flytte forsvinningspunktet:
.example {
perspective-origin: 50% 0%;
}
Helt supert. Nå ser det mer ut som om terningen ligger på et bord, og ikke svever i lufta.
PS! Tidligere nevnte jeg såvidt transform-style: preserve-3d;
. Nå er det lettere å
forklare hvorfor denne er viktig: Vi roterer kuben og hver side med separate transform
-regler. Uten å
spesifisere preserve-3d
ville disse blitt rotert uavhengig av hverandre. Nå
roteres de i samme kontekst.
La oss også slenge på noen tall på sidene:
<div class="example">
<div class="cube">
<div class="face face-1">1</div>
<div class="face face-2">2</div>
<div class="face face-3">3</div>
<div class="face face-4">4</div>
<div class="face face-5">5</div>
<div class="face face-6">6</div>
</div>
</div>
.face {
font-size: 60px;
line-height: 120px;
color: #aaa;
}
¶Så var det dette med kastingen da
For at en terning skal oppfattes som kastet må den:
- være i lufta
- rotere
- lande på en side
La oss starte på begynnelsen. For å gi inntrykk av å være i lufta uten å ta for mye plass på skjermen, så bestemte jeg meg for å zoome den vekk og litt opp. Her er animasjonsdefinisjonen:
@keyframes scale {
from { transform: scale3d(1, 1, 1) translate3d(0, 0, 0); }
50% { transform: scale3d(0.2, 0.2, 0.2) translate3d(0, -200px, 0); }
to { transform: scale3d(1, 1, 1) translate3d(0, 0, 0); }
}
Den starter på utgangsposisjonen, forsvinner ned til 20% i størrelse og opp 200px, før den kommer tilbake igjen.
Ettersom jeg ønsker at denne animasjonen skal skje uavhengig av hvordan
terningen er rotert, så må jeg gjøre skaleringen utenfor .cube
. Jeg
lager en .dice
:
<div class="example">
<div class="dice">
<div class="cube">
<div class="face face-1">1</div>
<div class="face face-2">2</div>
<div class="face face-3">3</div>
<div class="face face-4">4</div>
<div class="face face-5">5</div>
<div class="face face-6">6</div>
</div>
</div>
</div>
Den får samme regler som kuben, slik:
.dice,
.cube {
width: 120px;
height: 120px;
position: relative;
transform-style: preserve-3d;
display: inline-block;
}
Så gjelder det å koble inn animasjonen når terningen skal rulles:
.rolling.dice {
animation-name: scale;
animation-timing-function: ease-in-out;
animation-iteration-count: 1;
animation-duration: 1.8s;
}
Klikk på terningen for å “rulle” den. Scriptet legger her bare på klassen
rolling
- det er alt som skal til for å sparke animasjonen i gang. (klassen
fjernes også igjen etter et par sekunder, for at du skal kunne klikke flere ganger)
På tide å rotere terningen også. Vi kan starte med å finne hvordan kuben må roteres for å vise hver side:
.facing-1 { transform: rotateX(0deg) rotateY(0deg) rotateZ(0deg); }
.facing-2 { transform: rotateX(0deg) rotateY(-90deg) rotateZ(0deg); }
.facing-3 { transform: rotateX(-90deg) rotateY(-90deg) rotateZ(0deg); }
.facing-4 { transform: rotateX(-90deg) rotateY(180deg) rotateZ(90deg); }
.facing-5 { transform: rotateX(90deg) rotateY(180deg) rotateZ(90deg); }
.facing-6 { transform: rotateX(90deg) rotateY(0deg) rotateZ(0deg); }
Vi kan også legge på en transition, for syns skyld:
.cube {
transition: transform 600ms ease;
}
Klikk på terningen for å snu den til neste side.
Problemet med denne teknikken er at den ikke riktig fanger opplevelsen av en snurrende terning i lufta. Spesielt de første transisjonene var temmelig trauste. Men du la kanskje merke til at de påfølgende transisjonene hadde mer futt?
Trikset her er å rotere til riktig side, men å snurre litt ekstra mange ganger.
La oss si at vi skal snurre til side 1. Istedet for å gå til 0 0 0
kan vi gå
til 720 -360 360
. Det vil være samme side som vises, men kuben må rotere langt
mer for å komme seg dit.
Det kan jo også hende at terningen skal lande på samme side som den startet. Da må vi også sørge for at terningen ser ut til å snurre litt først.
Det jeg endte opp med var å definere animasjoner fra/til alle sider. Noe slikt:
@keyframes roll-1-to-1 {
from { transform: rotateX(0deg) rotateY(0deg) rotateZ(0deg); }
to { transform: rotateX(720deg) rotateY(0deg) rotateZ(0deg); }
}
@keyframes roll-1-to-2 {
from { transform: rotateX(0deg) rotateY(0deg) rotateZ(0deg); }
to { transform: rotateX(360deg) rotateY(-810deg) rotateZ(0deg); }
}
@keyframes roll-1-to-3 {
from { transform: rotateX(0deg) rotateY(0deg) rotateZ(0deg); }
to { transform: rotateX(-450deg) rotateY(-90deg) rotateZ(360deg); }
}
@keyframes roll-1-to-4 {
from { transform: rotateX(0deg) rotateY(0deg) rotateZ(0deg); }
to { transform: rotateX(-360deg) rotateY(180deg) rotateZ(-270deg); }
}
@keyframes roll-1-to-5 {
from { transform: rotateX(0deg) rotateY(0deg) rotateZ(0deg); }
to { transform: rotateX(-450deg) rotateY(540deg) rotateZ(450deg); }
}
@keyframes roll-1-to-6 {
from { transform: rotateX(0deg) rotateY(0deg) rotateZ(0deg); }
to { transform: rotateX(450deg) rotateY(360deg) rotateZ(0deg); }
}
Og så videre for 2-to-1
, 2-to-2
, 2-to-3
etc etc. Totalt 36 keyframes, med tilhørende css-klasser:
.roll-1-to-1 { animation-name: roll-1-to-1; }
.roll-1-to-2 { animation-name: roll-1-to-2; }
.roll-1-to-3 { animation-name: roll-1-to-3; }
.roll-1-to-4 { animation-name: roll-1-to-4; }
.roll-1-to-5 { animation-name: roll-1-to-5; }
.roll-1-to-6 { animation-name: roll-1-to-6; }
Her er animasjonsdeklarasjon for kuben:
.cube {
animation-timing-function: ease-in-out;
animation-iteration-count: 1;
animation-duration: 1.4s;
animation-fill-mode: both;
}
Og her kan du se de siste to triksene mine:
-
animation-fill-mode: both
Denne sørger for at terningen beholder posisjonen sin når den er ferdig animert. Uten denne vil terningen hoppe tilbake til utgangsposisjonen sin når animasjonen er ferdig.
-
animation-duration: 1.4s
Roteringen er satt til 1.4s, mens skaleringen er satt til 1.8s (lenger oppe i artikkelen). Dermed vil ikke kuben rotere hele veien - den stabiliserer seg mot slutten, og ser ut til å bli satt pent ned på bordet de siste 400ms.
Resultatet kan du se her:
.cube.facing-1
Klikk for å rulle terning.