Hvordan jeg lavede et rent CSS-puslespil PlatoBlockchain Data Intelligence. Lodret søgning. Ai.

Hvordan jeg lavede et rent CSS-puslespil

Jeg har for nylig opdaget glæden ved at skabe CSS-only-spil. Det er altid fascinerende, hvordan HTML og CSS er i stand til at håndtere logikken i et helt online spil, så jeg var nødt til at prøve det! Sådanne spil er normalt afhængige af det gamle afkrydsningsfelt-hack, hvor vi kombinerer den kontrollerede/umarkerede tilstand af et HTML-input med :checked pseudo-klasse i CSS. Vi kan gøre en masse magi med den ene kombination!

Faktisk udfordrede jeg mig selv til at bygge et helt spil uden Checkbox. Jeg var ikke sikker på, om det ville være muligt, men det er det bestemt, og jeg vil vise dig hvordan.

Ud over det puslespil, vi vil studere i denne artikel, har jeg lavet en samling af rene CSS-spil, de fleste af dem uden Checkbox Hack. (De er også tilgængelige på CodePen.)

Vil du spille før vi starter?

Jeg foretrækker personligt at spille spillet i fuldskærmstilstand, men du kan spille det nedenfor eller åbne den her.

Cool ikke? Jeg ved godt, at det ikke er det bedste puslespil, du nogensinde har set™, men det er heller ikke dårligt for noget, der kun bruger CSS og nogle få linjer HTML. Du kan nemt justere størrelsen på gitteret, ændre antallet af celler for at kontrollere sværhedsgraden og bruge det billede, du ønsker!

Vi skal lave den demo sammen, og derefter sætte lidt ekstra gnistre i den til sidst for nogle kicks.

Træk og slip-funktionen

Selvom strukturen af ​​puslespillet er ret ligetil med CSS Grid, er muligheden for at trække og slippe puslespilsbrikker en smule vanskeligere. Jeg var nødt til at stole på en kombination af overgange, svæveeffekter og søskendevælgere for at få det gjort.

Hvis du holder markøren over den tomme boks i den demo, flytter billedet sig inde i den og bliver der, selvom du flytter markøren ud af boksen. Tricket er at tilføje en stor overgangsvarighed og forsinkelse - så stor, at billedet tager lang tid at vende tilbage til sin oprindelige position.

img {
  transform: translate(200%);
  transition: 999s 999s; /* very slow move on mouseout */
}
.box:hover img {
  transform: translate(0);
  transition: 0s; /* instant move on hover */
}

Angiver kun transition-delay er nok, men at bruge store værdier på både forsinkelsen og varigheden mindsker chancen for, at en spiller nogensinde ser billedet flytte tilbage. Hvis du venter på 999s + 999s — hvilket er cirka 30 minutter — så vil du se billedet bevæge sig. Men det vil du ikke, vel? Jeg mener, ingen kommer til at tage så lang tid mellem svingene, medmindre de går væk fra spillet. Så jeg betragter dette som et godt trick til at skifte mellem to stater.

Har du bemærket, at det også udløser ændringerne ved at svæve over billedet? Det er fordi billedet er en del af kasseelementet, hvilket ikke er godt for os. Vi kan rette dette ved at tilføje pointer-events: none til billedet, men vi kan ikke trække det senere.

Det betyder, at vi skal introducere et andet element inde i .box:

Det ekstra div (vi bruger en klasse af .a) vil tage det samme område som billedet (takket være CSS Grid og grid-area: 1 / 1) og vil være det element, der udløser hover-effekten. Og det er her søskendevælgeren kommer i spil:

.a {
  grid-area: 1 / 1;
}
img {
  grid-area: 1 / 1;
  transform: translate(200%);
  transition: 999s 999s;
}
.a:hover + img {
  transform: translate(0);
  transition: 0s;
}

Svævende på .a element flytter billedet, og da det optager al plads inde i boksen, er det som om vi svæver over boksen i stedet for! At svæve over billedet er ikke længere et problem!

Lad os trække og slippe vores billede i boksen og se resultatet:

Så du det? Du griber først billedet og flytter det til boksen, ikke noget fancy. Men når først du slipper billedet, udløser du svæveeffekten, der flytter billedet, og så simulerer vi en træk og slip-funktion. Hvis du slipper musen uden for boksen, sker der ikke noget.

Hmm, din simulering er ikke perfekt, fordi vi også kan svæve boksen og få den samme effekt.

Sandt, og vi vil rette op på dette. Vi skal deaktivere hover-effekten og kun tillade den, hvis vi frigiver billedet inde i boksen. Vi vil lege med dimensionen af ​​vores .a element for at få det til at ske.

Nu gør det ikke noget at svæve boksen. Men hvis du begynder at trække billedet, vil den .a element vises, og når det først er frigivet inde i boksen, kan vi udløse svæveeffekten og flytte billedet.

Lad os dissekere koden:

.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;
}

Ved at klikke på billedet udløses :active pseudo-klasse, der gør .a element i fuld bredde (det er oprindeligt lig med 0). Den aktive tilstand forbliver aktiv indtil vi frigiver billedet. Hvis vi frigiver billedet inde i boksen, vil den .a element går tilbage til width: 0, men vi vil udløse svæveeffekten, før det sker, og billedet falder inde i boksen! Hvis du slipper den uden for boksen, sker der ikke noget.

Der er en lille særhed: at klikke på den tomme boks flytter også billedet og bryder vores funktion. I øjeblikket, :active er knyttet til .box element, så at klikke på det eller nogen af ​​dets børn vil aktivere det; og ved at gøre dette ender vi med at vise .a element og udløser hover-effekten.

Det kan vi ordne ved at lege med pointer-events. Det giver os mulighed for at deaktivere enhver interaktion med .box samtidig med at samspillet med barnets elementer bevares.

.box {
  pointer-events: none;
}
.box * {
  pointer-events: initial;
}

Nu vores træk og slip-funktion er perfekt. Medmindre du kan finde ud af, hvordan du hacker det, er den eneste måde at flytte billedet på ved at trække det og slippe det i boksen.

Opbygning af puslespillet

At lægge puslespillet vil føles let i forhold til, hvad vi lige har gjort for træk og slip-funktionen. Vi kommer til at stole på CSS-gitter og baggrundstricks for at skabe puslespillet.

Her er vores gitter, skrevet i Pug for nemheds skyld:

- 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") 

Koden ser måske mærkelig ud, men den kompileres til almindelig HTML:

<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>

Jeg vil vædde på, at du undrer dig over, hvad der er galt med de tags. Ingen af ​​disse elementer har nogen speciel betydning - jeg synes bare, at koden er meget nemmere at skrive ved hjælp af <z> end en flok <div class="z"> eller hvad som helst.

Sådan har jeg kortlagt dem:

  • <g> er vores grid container, der indeholder N*N <z> elementer.
  • <z> repræsenterer vores grid-emner. Det spiller rollen som .box element, vi så i forrige afsnit.
  • <a> udløser svæveeffekten.
  • <b> repræsenterer en del af vores billede. Vi anvender draggable attribut på den, fordi den ikke kan trækkes som standard.

Okay, lad os registrere vores netcontainer på <g>. Dette er i Sass i stedet for CSS:

$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);
}

Vi kommer faktisk til at lave vores grid-børn - de <z> elementer - også gitter og har begge dele <a> , <b> inden for samme netområde:

z {
  aspect-ratio: 1;
  display: grid;
  outline: 1px dashed;
}
a {
  grid-area: 1/1;
}
b {
  grid-area: 1/1;
}

Som du kan se, er der ikke noget fancy - vi har lavet et gitter med en bestemt størrelse. Resten af ​​den CSS, vi skal bruge, er til træk og slip-funktionen, som kræver, at vi tilfældigt placerer brikkerne rundt på brættet. Jeg vil henvende mig til Sass for dette, igen for bekvemmeligheden ved at kunne gå igennem og style alle puslespilsbrikkerne med en funktion:

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)
   }
}

Du har måske bemærket, at jeg bruger Sass random() fungere. Det er sådan, vi får de tilfældige positioner til puslespilsbrikkerne. Husk, at vi vil deaktivere den position, når du svæver over <a> element efter at have trukket og slippe dets tilsvarende <b> element inde i gittercellen.

z a:hover ~ b {
  transform: translate(0);
  transition: 0s;
}

I den samme løkke definerer jeg også baggrundskonfigurationen for hver brik i puslespillet. Alle vil logisk dele det samme billede som baggrunden, og dets størrelse skal være lig med størrelsen af ​​hele gitteret (defineret med --s variabel). Bruger det samme background-image og noget matematik, vi opdaterer background-position for kun at vise et stykke af billedet.

Det er det! Vores CSS-kun puslespil er teknisk færdigt!

Men vi kan altid gøre det bedre, ikke? Jeg viste dig hvordan man laver et gitter af puslespilsbrikker i en anden artikel. Lad os tage den samme idé og anvende den her, skal vi?

Former af puslespilsbrikker

Her er vores nye puslespil. Samme funktionalitet, men med mere realistiske former!

Dette er en illustration af formerne på gitteret:

Hvordan jeg lavede et rent CSS-puslespil

Hvis du ser godt efter, vil du bemærke, at vi har ni forskellige former for puslespilsbrikker: fire hjørner, fire kanterog en for alt andet.

Gitteret af puslespilsbrikker, jeg lavede i den anden artikel, jeg refererede til, er lidt mere ligetil:

Vi kan bruge den samme teknik, der kombinerer CSS-masker og gradienter til at skabe de forskellige former. Hvis du ikke er bekendt med mask og gradienter, jeg anbefaler stærkt at tjekke den forenklede sag for bedre at forstå teknikken, før du går videre til næste del.

Først skal vi bruge specifikke vælgere til at målrette mod hver gruppe af elementer, der deler den samme form. Vi har ni grupper, så vi vil bruge otte vælgere plus en standardvælger, der vælger dem alle.

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 */

Her er en figur, der viser, hvordan det knytter sig til vores gitter:

Hvordan jeg lavede et rent CSS-puslespil PlatoBlockchain Data Intelligence. Lodret søgning. Ai.
Hvordan jeg lavede et rent CSS-puslespil

Lad os nu tage fat på formerne. Lad os fokusere på at lære kun en eller to af figurerne, fordi de alle bruger den samme teknik - og på den måde har du nogle lektier, du kan blive ved med at lære!

For puslespilsbrikkerne i midten af ​​gitteret, 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);

Koden kan se kompleks ud, men lad os fokusere på én gradient ad gangen for at se, hvad der sker:

To gradienter skaber to cirkler (markeret grønt og lilla i demoen), og to andre gradienter skaber de spalter, som andre stykker forbinder til (den markerede blå fylder det meste af formen, mens den markerede rød fylder den øverste del). en CSS-variabel, --r, indstiller radius af de cirkulære former.

Hvordan jeg lavede et rent CSS-puslespil PlatoBlockchain Data Intelligence. Lodret søgning. Ai.
Hvordan jeg lavede et rent CSS-puslespil

Formen af ​​puslespilsbrikkerne i midten (markeret 0 i illustrationen) er den sværeste at lave, da den bruger fire gradienter og har fire krumninger. Alle de andre brikker jonglerer med færre gradienter.

For eksempel puslespilsbrikkerne langs den øverste kant af puslespillet (markeret 2 i illustrationen) bruger tre gradienter i stedet for fire:

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);

Vi fjernede den første (øverste) gradient og justerede værdierne for den anden gradient, så den dækker det efterladte rum. Du vil ikke bemærke den store forskel i koden, hvis du sammenligner de to eksempler. Det skal bemærkes, at vi kan finde forskellige baggrundskonfigurationer for at skabe den samme form. Hvis du begynder at lege med gradienter, vil du helt sikkert finde på noget andet end det jeg gjorde. Du kan endda skrive noget, der er mere kortfattet - hvis ja, del det i kommentarerne!

Udover at skabe formerne, vil du også opdage, at jeg øger bredden og/eller højden af ​​elementerne som nedenfor:

height: calc(100% + var(--r));
width: calc(100% + var(--r));

Puslespillets brikker skal flyde over deres gittercelle for at forbinde.

Hvordan jeg lavede et rent CSS-puslespil PlatoBlockchain Data Intelligence. Lodret søgning. Ai.
Hvordan jeg lavede et rent CSS-puslespil

Endelig demo

Her er den fulde demo igen. Hvis du sammenligner den med den første version, vil du se den samme kodestruktur til at oprette gitteret og træk-og-slip-funktionen, plus koden til at skabe formerne.

Mulige forbedringer

Artiklen slutter her, men vi kunne blive ved med at forbedre vores puslespil med endnu flere funktioner! Hvad med en timer? Eller måske en form for tillykke, når spilleren er færdig med puslespillet?

Jeg kan overveje alle disse funktioner i en fremtidig version, så hold øje med min GitHub-repo.

Indpakning op

og CSS er ikke et programmeringssprog, de siger. Ha!

Jeg forsøger ikke at udløse noget #HotDrama med det. Jeg siger det, fordi vi lavede nogle virkelig vanskelige logiske ting og dækkede en masse CSS-egenskaber og -teknikker undervejs. Vi legede med CSS Grid, overgange, maskering, gradienter, vælgere og baggrundsegenskaber. For ikke at nævne de få Sass-tricks, vi brugte til at gøre vores kode nem at justere.

Målet var ikke at bygge spillet, men at udforske CSS og opdage nye egenskaber og tricks, som du kan bruge i andre projekter. At skabe et onlinespil i CSS er en udfordring, der skubber dig til at udforske CSS-funktioner i detaljer og lære, hvordan du bruger dem. Derudover er det bare meget sjovt, at vi får noget at lege med, når alt er sagt og gjort.

Om CSS er et programmeringssprog eller ej, ændrer ikke det faktum, at vi altid lærer ved at bygge og skabe innovative ting.

Tidsstempel:

Mere fra CSS-tricks