Hvordan jeg laget et rent CSS-puslespill PlatoBlockchain Data Intelligence. Vertikalt søk. Ai.

Hvordan jeg laget et rent CSS-puslespill

Jeg oppdaget nylig gleden ved å lage CSS-bare spill. Det er alltid fascinerende hvordan HTML og CSS er i stand til å håndtere logikken til et helt online spill, så jeg måtte prøve det! Slike spill er vanligvis avhengige av den gamle Checkbox Hack hvor vi kombinerer den avkryssede/ukontrollerte tilstanden til en HTML-inndata med :checked pseudo-klasse i CSS. Vi kan gjøre mye magi med den ene kombinasjonen!

Faktisk utfordret jeg meg selv til å bygge et helt spill uten Checkbox. Jeg var ikke sikker på om det ville være mulig, men det er det definitivt, og jeg skal vise deg hvordan.

I tillegg til puslespillet vi skal studere i denne artikkelen, har jeg laget en samling rene CSS-spill, de fleste uten Checkbox Hack. (De er også tilgjengelige på CodePen.)

Vil du spille før vi starter?

Jeg personlig foretrekker å spille spillet i fullskjermmodus, men du kan spille det nedenfor eller åpne den opp her.

Kult ikke sant? Jeg vet, det er ikke det beste puslespillet du noensinne har sett™, men det er heller ikke dårlig i det hele tatt for noe som bare bruker CSS og noen få linjer med HTML. Du kan enkelt justere størrelsen på rutenettet, endre antall celler for å kontrollere vanskelighetsgraden, og bruke hvilket bilde du vil!

Vi skal lage den demoen på nytt sammen, og deretter sette litt ekstra gnisten i den på slutten for noen kick.

Dra og slipp-funksjonaliteten

Selv om strukturen til puslespillet er ganske grei med CSS Grid, er muligheten til å dra og slippe puslespillbiter litt vanskeligere. Jeg måtte stole på en kombinasjon av overganger, sveveeffekter og søskenvelgere for å få det til.

Hvis du holder musepekeren over den tomme boksen i den demoen, flytter bildet seg inn i den og forblir der selv om du flytter markøren ut av boksen. Trikset er å legge til en stor overgangsvarighet og forsinkelse - så stor at bildet tar mye tid å gå tilbake til utgangsposisjonen.

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

Spesifiserer kun transition-delay er nok, men å bruke store verdier på både forsinkelsen og varigheten reduserer sjansen for at en spiller noen gang ser bildet flytte seg tilbake. Hvis du venter på 999s + 999s — som er omtrent 30 minutter — så vil du se bildet bevege seg. Men du vil ikke, ikke sant? Jeg mener, ingen kommer til å ta så lang tid mellom svingene med mindre de går bort fra spillet. Så jeg anser dette som et godt triks for å bytte mellom to stater.

La du merke til at det å sveve bildet også utløser endringene? Det er fordi bildet er en del av bokselementet, noe som ikke er bra for oss. Vi kan fikse dette ved å legge til pointer-events: none til bildet, men vi kan ikke dra det senere.

Det betyr at vi må introdusere et annet element i .box:

Det ekstra div (vi bruker en klasse av .a) vil ta det samme området som bildet (takket være CSS Grid og grid-area: 1 / 1) og vil være elementet som utløser hover-effekten. Og det er her søskenvelgeren spiller inn:

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

Holder på .a element flytter bildet, og siden det tar opp all plass inne i boksen, er det som om vi svever over boksen i stedet! Å holde pekeren på bildet er ikke lenger et problem!

La oss dra og slippe bildet vårt i boksen og se resultatet:

Så du det? Du tar først bildet og flytter det til boksen, ikke noe fancy. Men når du slipper bildet, utløser du sveveeffekten som flytter bildet, og så simulerer vi en dra og slipp-funksjon. Hvis du slipper musen utenfor boksen, skjer det ingenting.

Hmm, simuleringen din er ikke perfekt fordi vi også kan sveve boksen og få samme effekt.

Sant og vi vil rette opp i dette. Vi må deaktivere sveveeffekten og tillate den bare hvis vi slipper bildet inne i boksen. Vi skal leke med dimensjonen vår .a element for å få det til.

Nå gjør det ingenting å sveve boksen. Men hvis du begynner å dra bildet, vil .a element vises, og når det er sluppet inne i boksen, kan vi utløse sveveeffekten og flytte bildet.

La oss 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 å klikke på bildet avfyres :active pseudo-klasse som gjør .a element i full bredde (det er i utgangspunktet lik 0). Den aktive tilstanden vil forbli aktiv til vi slipper bildet. Hvis vi slipper bildet inne i boksen, vil den .a element går tilbake til width: 0, men vi vil utløse sveveeffekten før det skjer og bildet faller inne i boksen! Hvis du slipper den utenfor boksen, skjer det ingenting.

Det er en liten finurlighet: å klikke på den tomme boksen flytter også bildet og bryter funksjonen vår. For tiden, :active er knyttet til .box element, så å klikke på det eller noen av dets barn vil aktivere det; og ved å gjøre dette ender vi opp med å vise .a element og utløser sveveeffekten.

Det kan vi fikse ved å leke med pointer-events. Det lar oss deaktivere enhver interaksjon med .box samtidig som interaksjonene med barneelementene opprettholdes.

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

vår dra og slipp-funksjon er perfekt. Med mindre du finner hvordan du hacker det, er den eneste måten å flytte bildet på å dra det og slippe det inne i boksen.

Bygge puslespillet rutenett

Å sette puslespillet vil føles enkelt sammenlignet med det vi nettopp gjorde for dra og slipp-funksjonen. Vi kommer til å stole på CSS-rutenett og bakgrunnstriks for å lage puslespillet.

Her er rutenettet vårt, skrevet i Pug for enkelhets 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 kan se merkelig ut, men den kompileres til vanlig 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 vedder på at du lurer på hva som skjer med disse kodene. Ingen av disse elementene har noen spesiell betydning - jeg synes bare at koden er mye lettere å skrive ved hjelp av <z> enn en haug med <div class="z"> eller hva som helst.

Dette er hvordan jeg har kartlagt dem:

  • <g> er vår nettbeholder som inneholder N*N <z> elementer.
  • <z> representerer våre rutenettelementer. Det spiller rollen som .box element vi så i forrige avsnitt.
  • <a> utløser sveveeffekten.
  • <b> representerer en del av bildet vårt. Vi anvender draggable attributt på den fordi den ikke kan dras som standard.

Greit, la oss registrere nettbeholderen vår 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 å lage våre nettbarn - de <z> elementer — rutenett også og har begge deler <a> og <b> innenfor samme rutenettområ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 det ikke noe fancy – vi laget et rutenett med en bestemt størrelse. Resten av CSS-en vi trenger er for dra og slipp-funksjonen, som krever at vi plasserer brikkene tilfeldig rundt brettet. Jeg kommer til å henvende meg til Sass for dette, igjen for bekvemmeligheten av å kunne gå gjennom og style alle puslespillbrikkene med en funksjon:

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 kanskje lagt merke til at jeg bruker Sass random() funksjon. Det er slik vi får de randomiserte posisjonene for puslespillbrikkene. Husk at vi vil deaktivere den posisjonen når du svever over <a> element etter å ha dra og slippe det tilsvarende <b> element inne i rutenettcellen.

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

I den samme løkken definerer jeg også bakgrunnskonfigurasjonen for hver brikke i puslespillet. Alle vil logisk dele det samme bildet som bakgrunnen, og størrelsen skal være lik størrelsen på hele rutenettet (definert med --s variabel). Bruker det samme background-image og litt matematikk, vi oppdaterer background-position for å vise bare en del av bildet.

Det er det! Vårt CSS-bare puslespill er teknisk ferdig!

Men vi kan alltid gjøre det bedre, ikke sant? Jeg viste deg hvordan lage et rutenett av puslespillbrikker i en annen artikkel. La oss ta den samme ideen og bruke den her, skal vi?

Former på puslespill

Her er vårt nye puslespill. Samme funksjonalitet, men med mer realistiske former!

Dette er en illustrasjon av formene på rutenettet:

Hvordan jeg laget et rent CSS-puslespill

Hvis du ser nøye etter, vil du legge merke til at vi har ni forskjellige puslespillbrikker: den fire hjørnerden fire kanterog en for alt annet.

Rutenettet med puslespillbrikker jeg laget i den andre artikkelen jeg refererte til er litt mer enkelt:

Vi kan bruke den samme teknikken som kombinerer CSS-masker og gradienter for å lage de forskjellige formene. I tilfelle du er ukjent med mask og gradienter, anbefaler jeg å sjekke den forenklede saken for bedre å forstå teknikken før du går videre til neste del.

Først må vi bruke spesifikke velgere for å målrette mot hver gruppe av elementer som deler samme form. Vi har ni grupper, så vi vil bruke åtte velgere, pluss en standardvelger som velger 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 som viser hvordan det tilordnes rutenettet vårt:

Hvordan jeg laget et rent CSS-puslespill PlatoBlockchain Data Intelligence. Vertikalt søk. Ai.
Hvordan jeg laget et rent CSS-puslespill

La oss nå takle formene. La oss fokusere på å lære bare én eller to av formene fordi de alle bruker samme teknikk - og på den måten har du noen lekser å fortsette å lære!

For puslespillbrikkene i midten av rutenettet, 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 ut, men la oss fokusere på én gradient om gangen for å se hva som skjer:

To gradienter skaper to sirkler (merket grønt og lilla i demoen), og to andre gradienter skaper sporene som andre brikker kobles til (den som er merket blå fyller opp mesteparten av formen mens den som er merket rød fyller den øverste delen). en CSS-variabel, --r, setter radiusen til de sirkulære formene.

Hvordan jeg laget et rent CSS-puslespill PlatoBlockchain Data Intelligence. Vertikalt søk. Ai.
Hvordan jeg laget et rent CSS-puslespill

Formen på puslespillbrikkene i midten (merket 0 i illustrasjonen) er den vanskeligste å lage siden den bruker fire gradienter og har fire krumninger. Alle de andre brikkene sjonglerer med færre gradienter.

For eksempel puslespillbitene langs den øvre kanten av puslespillet (merket 2 i illustrasjonen) bruker 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 fjernet den første (øverste) gradienten og justerte verdiene til den andre gradienten slik at den dekker plassen som er igjen. Du vil ikke merke noen stor forskjell i koden hvis du sammenligner de to eksemplene. Det skal bemerkes at vi kan finne forskjellige bakgrunnskonfigurasjoner for å lage den samme formen. Hvis du begynner å leke med gradienter vil du garantert finne på noe annet enn det jeg gjorde. Du kan til og med skrive noe som er mer kortfattet - hvis ja, del det i kommentarfeltet!

I tillegg til å lage formene, vil du også oppdage at jeg øker bredden og/eller høyden på elementene som nedenfor:

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

Brikkene i puslespillet må flyte over rutenettcellen deres for å koble til.

Hvordan jeg laget et rent CSS-puslespill PlatoBlockchain Data Intelligence. Vertikalt søk. Ai.
Hvordan jeg laget et rent CSS-puslespill

Endelig demo

Her er hele demoen igjen. Hvis du sammenligner den med den første versjonen, vil du se den samme kodestrukturen for å lage rutenettet og dra-og-slipp-funksjonen, pluss koden for å lage figurene.

Mulige forbedringer

Artikkelen slutter her, men vi kan fortsette å forbedre puslespillet vårt med enda flere funksjoner! Hva med en timer? Eller kanskje en slags gratulasjoner når spilleren fullfører puslespillet?

Jeg kan vurdere alle disse funksjonene i en fremtidig versjon, så hold øye med GitHub-repoen min.

Innpakning opp

Og CSS er ikke et programmeringsspråk, de sier. Ha!

Jeg prøver ikke å vekke #HotDrama med det. Jeg sier det fordi vi gjorde noen veldig vanskelige logiske ting og dekket mange CSS-egenskaper og -teknikker underveis. Vi lekte med CSS Grid, overganger, maskering, gradienter, velgere og bakgrunnsegenskaper. For ikke å nevne de få Sass-triksene vi brukte for å gjøre koden vår enkel å justere.

Målet var ikke å bygge spillet, men å utforske CSS og oppdage nye egenskaper og triks som du kan bruke i andre prosjekter. Å lage et nettspill i CSS er en utfordring som presser deg til å utforske CSS-funksjoner i detalj og lære hvordan du bruker dem. Dessuten er det bare veldig gøy at vi får noe å leke med når alt er sagt og gjort.

Om CSS er et programmeringsspråk eller ikke, endrer ikke det faktum at vi alltid lærer ved å bygge og skape innovative ting.

Tidstempel:

Mer fra CSS triks