Nemrég fedeztem fel a csak CSS-játékok készítésének örömét. Mindig lenyűgöző, hogy a HTML és a CSS mennyire képes kezelni egy egész online játék logikáját, ezért ki kellett próbálnom! Az ilyen játékok általában a régi Checkbox Hack-re támaszkodnak, ahol a HTML bemenet bejelölt/ellenőrzött állapotát kombináljuk a :checked
pszeudoosztály a CSS-ben. Ezzel az egyetlen kombinációval rengeteg varázslatot művelhetünk!
Valójában kihívtam magam, hogy egy egész játékot építsek Checkbox nélkül. Nem voltam benne biztos, hogy lehetséges-e, de határozottan az, és megmutatom, hogyan.
Amellett, hogy a kirakós játékot fogunk tanulmányozni ebben a cikkben, én készítettem tiszta CSS játékok gyűjteménye, legtöbbjük a Checkbox Hack nélkül. (Ezek is kaphatók a CodePen-en.)
Akarsz játszani, mielőtt elkezdenénk?
Én személy szerint jobban szeretem a játékot teljes képernyős módban játszani, de lent, ill nyisd ki itt.
Menő ugye? Tudom, hogy nem ez a legjobb kirakós játék, amit valaha látott™, de egyáltalán nem rossz valamihez, ami csak CSS-t és néhány soros HTML-t használ. Könnyedén beállíthatja a rács méretét, módosíthatja a cellák számát a nehézségi szint szabályozásához, és bármilyen képet használhat!
Együtt újrakészítjük ezt a demót, majd a végére egy kis extra csillogást teszünk bele néhány rúgás kedvéért.
A drag and drop funkció
Míg a rejtvény felépítése meglehetősen egyszerű a CSS Grid segítségével, a puzzle-darabok drag and drop funkciója kissé bonyolultabb. Átmenetek, lebegő effektusok és testvérválasztók kombinációjára kellett hagyatkoznom, hogy elvégezzem.
Ha a demóban az üres mezőre viszi az egérmutatót, a kép benne mozog, és akkor is ott marad, ha kimozdítja a kurzort a dobozból. A trükk az, hogy nagy átmeneti időtartamot és késleltetést adjunk hozzá – akkora, hogy a képnek sok időbe telik, amíg visszaáll a kiindulási helyzetébe.
img {
transform: translate(200%);
transition: 999s 999s; /* very slow move on mouseout */
}
.box:hover img {
transform: translate(0);
transition: 0s; /* instant move on hover */
}
Csak a transition-delay
elég, de a késleltetés és az időtartam nagy értékeinek használata csökkenti annak esélyét, hogy a játékos valaha is látja, hogy a kép visszamozdul. Ha vársz 999s + 999s
— ami körülbelül 30 perc — akkor látni fogja a kép mozgását. De nem fogod, igaz? Úgy értem, senkinek sem kell ilyen hosszú ideig tartania a kanyarokat, hacsak nem hagyja el a játékot. Szóval ezt jó trükknek tartom a két állapot közötti váltáshoz.
Észrevetted, hogy a kép lebegtetése is kiváltja a változásokat? Ez azért van, mert a kép a doboz elem része, ami nekünk nem jó. Ezt kiegészítéssel javíthatjuk pointer-events: none
a képre, de később nem tudjuk áthúzni.
Ez azt jelenti, hogy egy másik elemet kell bevezetnünk a .box
:
Az az extra div
(egy osztályt használunk .a
) ugyanazt a területet foglalja el, mint a kép (hála a CSS Gridnek és grid-area: 1 / 1
), és ez lesz az az elem, amely kiváltja a lebegési effektust. És itt jön képbe a testvérválasztó:
.a {
grid-area: 1 / 1;
}
img {
grid-area: 1 / 1;
transform: translate(200%);
transition: 999s 999s;
}
.a:hover + img {
transform: translate(0);
transition: 0s;
}
Lebeg a .a
elem mozgatja a képet, és mivel a dobozon belül minden helyet elfoglal, olyan, mintha a doboz fölött tartanánk az egérmutatót! A kép lebegtetése már nem probléma!
Húzzuk a képünket a dobozba, és nézzük meg az eredményt:
Láttad azt? Először megragadja a képet, és áthelyezi a dobozba, semmi különös. De ha egyszer elengedi a képet, akkor aktiválja a lebegtetési effektust, amely mozgatja a képet, majd szimulálunk egy fogd és vidd funkciót. Ha elengedi az egeret a dobozon kívül, semmi sem történik.
Hmm, a szimulációd nem tökéletes, mert mi is lebegtethetjük a dobozt, és ugyanazt a hatást érhetjük el.
Ez igaz, és ezt orvosolni fogjuk. Ki kell kapcsolnunk a lebegés effektust, és csak akkor engedjük meg, ha kiengedjük a képet a dobozon belül. Játszunk majd a miénk dimenziójával .a
elemet, hogy ez megtörténjen.
Most a doboz lebegtetése nem tesz semmit. De ha elkezdi húzni a képet, a .a
elem jelenik meg, és miután kiengedjük a dobozon belül, elindíthatjuk a lebegés effektust és mozgathatjuk a képet.
Boncoljuk a kódot:
.a {
width: 0%;
transition: 0s .2s; /* add a small delay to make sure we catch the hover effect */
}
.box:active .a { /* on :active increase the width */
width: 100%;
transition: 0s; /* instant change */
}
img {
transform: translate(200%);
transition: 999s 999s;
}
.a:hover + img {
transform: translate(0);
transition: 0s;
}
A képre kattintva beindul a :active
pszeudoosztály, amely a .a
elem teljes szélességű (kezdetben egyenlő 0
). Az aktív állapot megmarad aktív amíg el nem engedjük a képet. Ha kiengedjük a képet a dobozon belül, a .a
elemre tér vissza width: 0
, de kiváltjuk a lebegő effektust, mielőtt ez megtörténne, és a kép a dobozba esik! Ha kiengedi a dobozon kívül, nem történik semmi.
Van egy kis furcsaság: az üres mezőre kattintva a kép is elmozdul, és megszakad a funkciónk. Jelenleg :active
kapcsolódik a .box
elemet, így ha rá vagy valamelyik gyermekére kattint, az aktiválódik; és ezzel a végén megmutatjuk a .a
elemet, és kiváltja a lebegő effektust.
Ezt úgy tudjuk megoldani, ha játszunk vele pointer-events
. Lehetővé teszi számunkra, hogy letiltsunk minden interakciót a .box
miközben fenntartja a gyermekelemekkel való interakciókat.
.box {
pointer-events: none;
}
.box * {
pointer-events: initial;
}
Most a drag and drop funkciónk tökéletes. Hacsak nem találja meg, hogyan lehet feltörni, csak úgy mozgathatja a képet, ha húzza és ejtse a dobozba.
A puzzle rács építése
A puzzle összerakása egyszerű lesz ahhoz képest, amit a drag and drop funkciónál tettünk. A rejtvény elkészítéséhez CSS-rácsra és háttértrükkökre fogunk támaszkodni.
Íme a rácsunk, mopsz nyelven írva a kényelem kedvéért:
- let n = 4; /* number of columns/rows */
- let image = "https://picsum.photos/id/1015/800/800";
g(style=`--i:url(${image})`)
- for(let i = 0; i < n*n; i++)
z
a
b(draggable="true")
A kód furcsán nézhet ki, de egyszerű HTML-be fordítja le:
<g style="--i: url(https://picsum.photos/id/1015/800/800)">
<z>
<a></a>
<b draggable="true"></b>
</z>
<z>
<a></a>
<b draggable="true"></b>
</z>
<z>
<a></a>
<b draggable="true"></b>
</z>
<!-- etc. -->
</g>
Fogadok, hogy kíváncsi, mi van ezekkel a címkékkel. Ezeknek az elemeknek nincs különösebb jelentése – csak azt tapasztalom, hogy a kódot sokkal könnyebb megírni <z>
mint egy csomó <div class="z">
vagy mindegy.
Így térképeztem fel őket:
<g>
a mi rácstartályunk, amely tartalmazzaN*N
<z>
elemek.<z>
rácselemeinket képviseli. Szerepét játssza a.box
elemet láttunk az előző részben.<a>
kiváltja a lebegés effektust.<b>
képünk egy részét képviseli. Alkalmazzuk adraggable
attribútumot, mert alapértelmezés szerint nem húzható.
Rendben, regisztráljuk a rácstárolónkat <g>
. Ez a Sass nyelven van CSS helyett:
$n : 4; /* number of columns/rows */
g {
--s: 300px; /* size of the puzzle */
display: grid;
max-width: var(--s);
border: 1px solid;
margin: auto;
grid-template-columns: repeat($n, 1fr);
}
Valójában a rácsunk gyermekeit fogjuk csinálni – a <z>
elemeket – rácsokat is, és mindkettőt <a>
és a <b>
ugyanazon a rácsterületen belül:
z {
aspect-ratio: 1;
display: grid;
outline: 1px dashed;
}
a {
grid-area: 1/1;
}
b {
grid-area: 1/1;
}
Amint látja, semmi különös – egy meghatározott méretű rácsot hoztunk létre. A többi CSS-re a drag and drop funkcióhoz van szükségünk, ami megköveteli, hogy véletlenszerűen helyezzük el a darabokat a tábla körül. Ehhez ismét Sasshoz fogok fordulni, még egyszer a kényelem kedvéért, hogy az összes puzzle-darabot egy funkcióval átnézhessem és stílusozhassam:
b {
background: var(--i) 0/var(--s) var(--s);
}
@for $i from 1 to ($n * $n + 1) {
$r: (random(180));
$x: (($i - 1)%$n);
$y: floor(($i - 0.001) / $n);
z:nth-of-type(#{$i}) b{
background-position: ($x / ($n - 1)) * 100% ($y / ($n - 1)) * 100%;
transform:
translate((($n - 1) / 2 - $x) * 100%, (($n - 1)/2 - $y) * 100%)
rotate($r * 1deg)
translate((random(100)*1% + ($n - 1) * 100%))
rotate((random(20) - 10 - $r) * 1deg)
}
}
Talán észrevetted, hogy a Sasst használom random()
funkció. Így kapjuk meg a puzzle-darabok véletlenszerű pozícióit. Ne feledje, hogy fogunk letiltása abban a helyzetben, amikor az egérmutatót a <a>
elemet a megfelelő húzás után <b>
elem a rácscellán belül.
z a:hover ~ b {
transform: translate(0);
transition: 0s;
}
Ugyanebben a ciklusban a háttérkonfigurációt is meghatározom a puzzle minden egyes darabjához. Logikailag mindegyik ugyanazt a képet fogja megosztani, mint a háttér, és méretének meg kell egyeznie a teljes rács méretével (a --s
változó). Ugyanazt használva background-image
és némi matek, frissítjük a background-position
hogy a képnek csak egy darabja jelenjen meg.
Ez az! A csak CSS-t használó kirakós játékunk technikailag elkészült!
De mindig tudunk jobbat csinálni, igaz? megmutattam hogyan készítsünk egy rácsot a kirakós darab formákból egy másik cikkben. Vegyük ugyanezt az ötletet, és alkalmazzuk itt is, jó?
Puzzle darab formák
Íme az új kirakós játékunk. Ugyanaz a funkcionalitás, de valósághűbb formákkal!
Ez a rácson lévő alakzatok illusztrációja:
Ha alaposan megnézed, észre fogod venni, hogy kilenc különböző kirakós formánk van: a négy sarok, a négy élés egy minden máshoz.
A kirakós darabokból álló rács, amelyet a másik hivatkozott cikkben készítettem, egy kicsit egyszerűbb:
Ugyanazt a technikát használhatjuk, amely a CSS-maszkokat és a színátmeneteket kombinálja a különböző alakzatok létrehozásához. Abban az esetben, ha nem ismeri mask
és színátmenetek, erősen ajánlom az ellenőrzést az az egyszerűsített eset hogy jobban megértsük a technikát, mielőtt a következő részre lépnénk.
Először is speciális szelektorokat kell használnunk, hogy megcélozzuk az azonos alakú elemcsoportokat. Kilenc csoportunk van, tehát nyolc választót fogunk használni, plusz egy alapértelmezett választót, amely mindegyiket kiválasztja.
z /* 0 */
z:first-child /* 1 */
z:nth-child(-n + 4):not(:first-child) /* 2 */
z:nth-child(5) /* 3 */
z:nth-child(5n + 1):not(:first-child):not(:nth-last-child(5)) /* 4 */
z:nth-last-child(5) /* 5 */
z:nth-child(5n):not(:nth-child(5)):not(:last-child) /* 6 */
z:last-child /* 7 */
z:nth-last-child(-n + 4):not(:last-child) /* 8 */
Íme egy ábra, amely bemutatja, hogy ez hogyan illeszkedik a rácsunkhoz:
Most foglalkozzunk a formákkal. Koncentráljunk arra, hogy csak egy vagy kettő alakzatot tanuljunk meg, mert mindegyik ugyanazt a technikát használja – és így van egy kis házi feladatod a tanuláshoz!
A rács közepén lévő puzzle-darabokhoz 0
:
mask:
radial-gradient(var(--r) at calc(50% - var(--r) / 2) 0, #0000 98%, #000) var(--r)
0 / 100% var(--r) no-repeat,
radial-gradient(var(--r) at calc(100% - var(--r)) calc(50% - var(--r) / 2), #0000 98%, #000)
var(--r) 50% / 100% calc(100% - 2 * var(--r)) no-repeat,
radial-gradient(var(--r) at var(--r) calc(50% - var(--r) / 2), #000 98%, #0000),
radial-gradient(var(--r) at calc(50% + var(--r) / 2) calc(100% - var(--r)), #000 98%, #0000);
A kód bonyolultnak tűnhet, de összpontosítsunk egyszerre egy színátmenetre, hogy lássuk, mi történik:
Két színátmenet két kört hoz létre (a bemutatóban zölddel és lilával jelölve), két másik színátmenet pedig azokat a nyílásokat, amelyekhez a többi darab csatlakozik (a kékkel jelölt az alakzat nagy részét, míg a pirossal jelölt a felső részt). CSS-változó, --r
, beállítja a kör alakzatok sugarát.
A puzzle darabjainak formája a közepén (jelölve 0
az ábrán) a legnehezebb elkészíteni, mivel négy színátmenetet használ, és négy görbülete van. Az összes többi darab kevesebb színátmenettel zsonglőrködik.
Például a puzzle darabjai a puzzle felső széle mentén (jelölve 2
az ábrán) három színátmenetet használ négy helyett:
mask:
radial-gradient(var(--r) at calc(100% - var(--r)) calc(50% + var(--r) / 2), #0000 98%, #000) var(--r) calc(-1 * var(--r)) no-repeat,
radial-gradient(var(--r) at var(--r) calc(50% - var(--r) / 2), #000 98%, #0000),
radial-gradient(var(--r) at calc(50% + var(--r) / 2) calc(100% - var(--r)), #000 98%, #0000);
Az első (felső) színátmenetet eltávolítottuk, a második színátmenet értékeit pedig úgy módosítottuk, hogy az lefedje a hátrahagyott teret. Nem fog nagy különbséget észrevenni a kódban, ha összehasonlítja a két példát. Megjegyzendő, hogy különböző háttérkonfigurációkat találhatunk ugyanazon alakzat létrehozásához. Ha elkezdesz játszani a színátmenetekkel, biztosan valami mást fogsz kitalálni, mint amit én csináltam. Akár tömörebbet is írhatsz – ha igen, oszd meg kommentben!
Az alakzatok létrehozása mellett azt is tapasztalhatja, hogy az alábbiak szerint növelem az elemek szélességét és/vagy magasságát:
height: calc(100% + var(--r));
width: calc(100% + var(--r));
A kirakós daraboknak túl kell csordulniuk a rácscellánkon, hogy összekapcsolódjanak.
Végső demó
Ismét itt a teljes demó. Ha összehasonlítja az első verzióval, ugyanazt a kódstruktúrát fogja látni a rács létrehozásához és a fogd és vidd funkcióhoz, valamint az alakzatok létrehozásához szükséges kódot.
Lehetséges fejlesztések
A cikk itt ér véget, de még több funkcióval bővíthetnénk rejtvényünket! Mit szólnál az időzítőhöz? Vagy esetleg valamiféle gratuláció, amikor a játékos befejezi a rejtvényt?
Lehetséges, hogy ezeket a funkciókat egy jövőbeli verzióban figyelembe veszem, így tartsa szemmel a GitHub-repót.
Csomagolta
És A CSS nem programozási nyelv, azt mondják. Ha!
Nem próbálok ezzel valami #HotDrama-t kelteni. Azért mondom, mert nagyon trükkös logikai dolgokat csináltunk, és sok CSS tulajdonságot és technikát lefedtünk az út során. Játszottunk a CSS Griddel, az átmenetekkel, a maszkolással, a színátmenetekkel, a kiválasztókkal és a háttértulajdonságokkal. Nem is beszélve arról a néhány Sass-trükkről, amellyel a kódunkat könnyen beállíthattuk.
A cél nem a játék felépítése volt, hanem a CSS felfedezése, és olyan új tulajdonságok és trükkök felfedezése, amelyeket más projektekben is felhasználhat. Online játék létrehozása CSS-ben olyan kihívás, amely arra készteti, hogy alaposan fedezze fel a CSS-funkciókat, és tanulja meg használatukat. Ráadásul nagyon mulatságos, hogy ha mindennel elhangzik, van mit játszani.
Az, hogy a CSS programozási nyelv-e vagy sem, nem változtat azon a tényen, hogy mindig innovatív dolgok építésével és létrehozásával tanulunk.