Creați un glisor personalizat pentru pagină completă cu CSS și JavaScript

Publicat: 2022-03-11

Lucrez foarte mult cu aspecte personalizate pe tot ecranul, practic zilnic. De obicei, aceste aspecte implică o cantitate substanțială de interacțiune și animație. Fie că este vorba de o cronologie complexă de tranziții declanșată de timp sau de un set de evenimente bazat pe defilare, condus de utilizator, în cele mai multe cazuri, interfața de utilizare necesită mai mult decât utilizarea unei soluții de plugin ieșită din cutie, cu câteva modificări și modificări. . Pe de altă parte, văd că mulți dezvoltatori JavaScript tind să apeleze la pluginul lor JS favorit pentru a le ușura munca, chiar dacă sarcina poate să nu aibă nevoie de toate clopotele și fluierele oferite de un anumit plugin.

Disclaimer: Folosirea unuia dintre multele pluginuri disponibile are avantajele sale, desigur. Veți obține o varietate de opțiuni pe care le puteți utiliza pentru a vă adapta nevoilor fără a fi nevoie să faceți multă codare. De asemenea, majoritatea autorilor de plugin-uri își optimizează codul, îl fac compatibil între browsere și platforme și așa mai departe. Dar totuși, obțineți o bibliotecă de dimensiune completă inclusă în proiectul dvs. pentru poate doar unul sau două lucruri diferite pe care le oferă. Nu spun că utilizarea unui plugin terță parte de orice fel este în mod natural un lucru rău, o fac zilnic în proiectele mele, doar că, în general, este o idee bună să cântărim avantajele și dezavantajele fiecărei abordări așa cum este. o bună practică în codificare. Când vine vorba de a-ți face propriul lucru în acest fel, este nevoie de puțin mai multe cunoștințe de codare și experiență pentru a ști ce cauți, dar în cele din urmă, ar trebui să obții o bucată de cod care face un lucru și un lucru doar așa. vrei tu.

Acest articol are scopul de a arăta o abordare CSS/JS pură în dezvoltarea unui aspect de glisor declanșat de defilare pe ecran complet cu animație de conținut personalizată. În această abordare redusă, voi acoperi structura HTML de bază la care v-ați aștepta să fie livrată dintr-un back-end CMS, tehnici moderne de aspect CSS (SCSS) și codare JavaScript vanilla pentru interactivitate deplină. Fiind simplu, acest concept poate fi extins cu ușurință la un plugin la scară mai mare și/sau utilizat într-o varietate de aplicații care nu au dependențe la bază.

Designul pe care îl vom crea este o vitrină de portofoliu de arhitecți minimaliste, cu imagini prezentate și titluri ale fiecărui proiect. Glisorul complet cu animații va arăta astfel:

Exemplu de glisor al unui portofoliu de arhitecți.

Puteți consulta demonstrația aici și puteți accesa depozitul meu Github pentru mai multe detalii.

Prezentare generală HTML

Deci, iată HTML-ul de bază cu care vom lucra:

 <div> <div class="mask"> <!-- Textual logo will go here --> </div> <div> <div class="slides"> <!-- Featured image slides will go here --> </div> <div class="slides mask"> <!-- Slide titles will go here --> </div> </div> <div> <!-- Static info on the right --> </div> <nav> <!-- Current slide indicator --> </nav> </div>

Un div cu id-ul de hero-slider este deținătorul nostru principal. În interior, aspectul este împărțit în secțiuni:

  • Logo (o secțiune statică)
  • Prezentare de diapozitive la care vom lucra mai ales
  • Informații (o secțiune statică)
  • Slider nav care va indica diapozitivul activ în prezent, precum și numărul total de diapozitive

Să ne concentrăm pe secțiunea de prezentare, deoarece acesta este punctul nostru de interes în acest articol. Aici avem două părți — principal și auxiliar . Main este div-ul care conține imagini prezentate, în timp ce auxiliar deține titlurile imaginilor. Structura fiecărui diapozitiv din interiorul acestor două suporturi este destul de simplă. Aici avem un diapozitiv de imagine în interiorul suportului principal:

 <div class="slide" data-index="0"> <div class="abs-mask"> <div class="slide-image"> </div> </div> </div>

Atributul de date index este ceea ce vom folosi pentru a urmări unde ne aflăm în prezentare. Div-ul cu mască abs pe care îl vom folosi pentru a crea un efect de tranziție interesant, iar div-ul cu imagine de diapozitiv conține imaginea prezentată specifică. Imaginile sunt redate inline ca și cum ar proveni direct dintr-un CMS și sunt setate de utilizatorul final.

În mod similar, titlul alunecă în interiorul suportului auxiliar:

 <h2 class="slide-title slide" data-index="0"><a href="#">#64 Paradigm</a></h2>

Fiecare titlu de diapozitiv este o etichetă H2 cu atributul de date corespunzător și un link pentru a putea duce la pagina unică a proiectului respectiv.

Restul HTML-ului nostru este, de asemenea, destul de simplu. Avem un logo în partea de sus, informații statice care îi spun utilizatorului pe ce pagină se află, o descriere și un indicator de curent/total al glisorului.

Prezentare generală CSS

Codul CSS sursă este scris în SCSS, un pre-procesor CSS care este apoi compilat în CSS obișnuit pe care browserul îl poate interpreta. SCSS vă oferă avantajul de a utiliza variabile, selecție imbricată, mixuri și alte lucruri interesante, dar trebuie compilat în CSS pentru ca browserul să citească codul așa cum ar trebui. În scopul acestui tutorial, am folosit Scout-App pentru a gestiona compilarea, deoarece mi-am dorit să am instrumentele la minimum.

Am folosit flexbox pentru a gestiona aspectul de bază unul lângă altul. Ideea este să ai slideshow-ul pe o parte și secțiunea de informații pe cealaltă.

 #hero-slider { position: relative; height: 100vh; display: flex; background: $dark-color; } #slideshow { position: relative; flex: 1 1 $main-width; display: flex; align-items: flex-end; padding: $offset; } #info { position: relative; flex: 1 1 $side-width; padding: $offset; background-color: #fff; }

Să ne aprofundăm în poziționare și, din nou, să ne concentrăm pe secțiunea de prezentare:

 #slideshow { position: relative; flex: 1 1 $main-width; display: flex; align-items: flex-end; padding: $offset; } #slides-main { @extend %abs; &:after { content: ''; @extend %abs; background-color: rgba(0, 0, 0, .25); z-index: 100; } .slide-image { @extend %abs; background-position: center; background-size: cover; z-index: -1; } } #slides-aux { position: relative; top: 1.25rem; width: 100%; .slide-title { position: absolute; z-index: 300; font-size: 4vw; font-weight: 700; line-height: 1.3; @include outlined(#fff); } }

Am setat glisorul principal să fie poziționat absolut și ca imaginile de fundal să întindă întreaga zonă folosind proprietatea background-size: cover . Pentru a oferi mai mult contrast față de titlurile diapozitivelor, am setat un pseudo-element absolut care acționează ca o suprapunere. Glisorul auxiliar care conține titlurile diapozitivelor este poziționat în partea de jos a ecranului și deasupra imaginilor.

Deoarece doar un diapozitiv va fi vizibil la un moment dat, am setat fiecare titlu să fie și absolut absolut și am calculat dimensiunea suportului prin JS pentru a mă asigura că nu există limite, dar mai multe despre asta într-una dintre secțiunile noastre viitoare. Aici puteți vedea utilizarea unei caracteristici SCSS numită extindere:

 %abs { position: absolute; top: 0; left: 0; height: 100%; width: 100%; }

Deoarece am folosit foarte mult poziționarea absolută, am tras acest CSS într-un extensibil pentru a-l avea ușor disponibil în diferite selectoare. De asemenea, am creat un mixin numit „conturat” pentru a oferi o abordare DRY atunci când modelați titlurile și titlul cursorului principal.

 @mixin outlined($color: $dark-color, $size: 1px) { color: transparent; -webkit-text-stroke: $size $color; }

În ceea ce privește partea statică a acestui aspect, nu are nimic complex, dar aici puteți vedea o metodă interesantă atunci când poziționați textul care trebuie să fie pe axa Y în loc de fluxul său normal:

 .slider-title-wrapper { position: absolute; top: $offset; left: calc(100% - #{$offset}); transform-origin: 0% 0%; transform: rotate(90deg); @include outlined; }

Aș dori să vă atrag atenția asupra proprietății transform-origin deoarece am găsit-o cu adevărat subutilizată pentru acest tip de aspect. Modul în care este poziționat acest element este că ancora sa rămâne în colțul din stânga sus al elementului, setând punctul de rotație și având textul să curgă continuu din acel punct în jos, fără probleme când vine vorba de diferite dimensiuni ale ecranului.

Să aruncăm o privire la o parte CSS mai interesantă - animația de încărcare inițială:

Încărcați animația pentru glisor.

De obicei, acest tip de comportament de animație sincronizat se realizează folosind o bibliotecă - GSAP, de exemplu, este una dintre cele mai bune disponibile, oferind capabilități excelente de randare, este ușor de utilizat și are funcționalitatea cronologică care permite dezvoltatorului să înlănțească programatic elementele. tranziții unul în celălalt.

Cu toate acestea, deoarece acesta este un exemplu pur CSS/JS, am decis să merg cu adevărat de bază aici. Deci, fiecare element este setat la poziția sa de pornire în mod implicit – fie ascuns prin transformare, fie opacitate și afișat la încărcarea cursorului care este declanșată de JS-ul nostru. Toate proprietățile de tranziție sunt modificate manual pentru a asigura un flux natural și interesant, fiecare tranziție continuând în alta, oferind o experiență vizuală plăcută.

 #logo:after { transform: scaleY(0); transform-origin: 50% 0; transition: transform .35s $easing; } .logo-text { display: block; transform: translate3d(120%, 0, 0); opacity: 0; transition: transform .8s .2s, opacity .5s .2s; } .current, .sep:before { opacity: 0; transition: opacity .4s 1.3s; } #info { transform: translate3d(100%, 0, 0); transition: transform 1s $easing .6s; } .line { transform-origin: 0% 0; transform: scaleX(0); transition: transform .7s $easing 1s; } .slider-title { overflow: hidden; >span { display: block; transform: translate3d(0, -100%, 0); transition: transform .5s 1.5s; } }

Dacă este un lucru pe care aș dori să vedeți aici, este utilizarea proprietății transform . Când mutați un element HTML, fie că este o tranziție sau o animație, se recomandă utilizarea proprietății transform . Văd o mulțime de oameni care tind să folosească fie marginea, fie căptușeala sau chiar offset-urile – sus, stânga, etc., ceea ce nu produce rezultate adecvate când vine vorba de randare.

Pentru a obține o înțelegere mai aprofundată a modului de utilizare a CSS atunci când adăugați un comportament interactiv, nu aș putea recomanda suficient următorul articol.

Este de Paul Lewis, un inginer Chrome, și acoperă aproape tot ce ar trebui să știți despre redarea pixelilor în web, indiferent dacă este vorba de CSS sau JS.

Prezentare generală JavaScript și logica slider

Fișierul JavaScript este împărțit în două funcții distincte.

Funcția heroSlider care are grijă de toate funcționalitățile de care avem nevoie aici și funcția utils la care am adăugat câteva funcții utilitare reutilizabile. Am comentat fiecare dintre aceste funcții utilitare pentru a oferi context dacă doriți să le reutilizați în proiectul dvs.

Funcția principală este codificată astfel încât să aibă două ramuri: init și resize . Aceste ramuri sunt disponibile prin returnarea funcției principale și sunt invocate atunci când este necesar. init este inițializarea funcției principale și este declanșată la evenimentul de încărcare a ferestrei. În mod similar, ramura de redimensionare este declanșată la redimensionarea ferestrei. Singurul scop al funcției de redimensionare este de a recalcula dimensiunea glisorului titlului la redimensionarea ferestrei, deoarece dimensiunea fontului titlului poate varia.

În funcția heroSlider , am furnizat un obiect glisor care conține toate datele și selectoarele de care vom avea nevoie:

 const slider = { hero: document.querySelector('#hero-slider'), main: document.querySelector('#slides-main'), aux: document.querySelector('#slides-aux'), current: document.querySelector('#slider-nav .current'), handle: null, idle: true, activeIndex: -1, interval: 3500 };

Ca o notă secundară, această abordare ar putea fi adaptată cu ușurință dacă, de exemplu, utilizați React, deoarece puteți stoca datele în stare sau puteți utiliza cârligele nou adăugate. Pentru a rămâne la curent, să vedem ceea ce reprezintă fiecare dintre perechile cheie-valoare de aici:

  • Primele patru proprietăți sunt o referință HTML la elementul DOM pe care îl vom manipula.
  • Proprietatea handle va fi folosită pentru a porni și opri funcționalitatea redării automate.
  • Proprietatea idle este un indicator care va împiedica utilizatorul să forțeze derularea în timp ce slide-ul este în tranziție.
  • activeIndex ne va permite să urmărim slide-ul activ în prezent
  • interval denotă intervalul de redare automată al glisorului

La inițializarea cursorului, invocăm două funcții:

 setHeight(slider.aux, slider.aux.querySelectorAll('.slide-title')); loadingAnimation();

Funcția setHeight ajunge la o funcție de utilitate pentru a seta înălțimea glisorului nostru auxiliar pe baza dimensiunii maxime a titlului. În acest fel, ne asigurăm că este oferită o dimensiune adecvată și niciun titlu de diapozitiv nu va fi tăiat chiar și atunci când conținutul său scade în două rânduri.

Funcția loadingAnimation adaugă o clasă CSS la elementul care furnizează tranzițiile CSS introductive:

 const loadingAnimation = function () { slider.hero.classList.add('ready'); slider.current.addEventListener('transitionend', start, { once: true }); }

Deoarece indicatorul nostru glisor este ultimul element din cronologia tranziției CSS, așteptăm ca tranziția sa se încheie și invocăm funcția de pornire. Prin furnizarea unui parametru suplimentar ca obiect, ne asigurăm că acesta este declanșat o singură dată.

Să aruncăm o privire la funcția de pornire:

 const start = function () { autoplay(true); wheelControl(); window.innerWidth <= 1024 && touchControl(); slider.aux.addEventListener('transitionend', loaded, { once: true }); }

Deci, când aspectul s-a terminat, tranziția sa inițială este declanșată de funcția loadingAnimation și funcția de pornire preia. Apoi declanșează funcționalitatea de redare automată, permite controlul roții, determină dacă ne aflăm pe un dispozitiv tactil sau desktop și așteaptă prima tranziție din diapozitivul titlurilor pentru a adăuga clasa CSS corespunzătoare.

Redare automata

Una dintre caracteristicile de bază ale acestui aspect este funcția de redare automată. Să trecem peste funcția corespunzătoare:

 const autoplay = function (initial) { slider.autoplay = true; slider.items = slider.hero.querySelectorAll('[data-index]'); slider.total = slider.items.length / 2; const loop = () => changeSlide('next'); initial && requestAnimationFrame(loop); slider.handle = utils().requestInterval(loop, slider.interval); }

Mai întâi, setăm indicatorul de redare automată la adevărat, indicând că glisorul este în modul de redare automată. Acest semnalizare este utilă atunci când se determină dacă se reactivează redarea automată după ce utilizatorul interacționează cu glisorul. Apoi facem referire la toate elementele slider (diapozitive), deoarece le vom schimba clasa activă și vom calcula numărul total de iterații pe care glisorul le va avea adunând toate elementele și împărțind la două, deoarece avem două aspecte sincronizate de glisor (principal și auxiliar) ci un singur „glisor” per se care le schimbă pe amândouă simultan.

Cea mai interesantă parte a codului de aici este funcția buclă. slideChange , oferind direcția de alunecare pe care o vom parcurge într-un minut, cu toate acestea, funcția buclă este numită de câteva ori. Să vedem de ce.

Dacă argumentul inițial este evaluat ca fiind adevărat, vom invoca funcția buclă ca apel invers requestAnimationFrame . Acest lucru se întâmplă doar la prima încărcare a glisorului care declanșează schimbarea imediată a glisării. Folosind requestAnimationFrame , executăm apelul înapoi furnizat chiar înainte de următoarea revopsire a cadrului.

Diagrama pașilor utilizați pentru a crea glisorul.

Cu toate acestea, deoarece dorim să continuăm să parcurgem diapozitive în modul de redare automată, vom folosi un apel repetat al aceleiași funcții. Acest lucru se realizează de obicei cu setInterval. Dar, în acest caz, vom folosi una dintre funcțiile utilitare requestInterval . În timp ce setInterval ar funcționa foarte bine, requestInterval este un concept avansat care se bazează pe requestAnimationFrame și oferă o abordare mai performantă. Se asigură că funcția este re-declanșată numai dacă fila browser este activă.

Mai multe despre acest concept în acest articol minunat puteți găsi pe trucuri CSS. Vă rugăm să rețineți că atribuim valoarea returnată de la această funcție proprietății noastre slider.handle . Acest ID unic pe care îl returnează funcția este disponibil pentru noi și îl vom folosi pentru a anula redarea automată mai târziu folosind cancelAnimationFrame .

Schimbare de diapozitive

Funcția slideChange este funcția principală în întregul concept. Schimbă diapozitivele fie prin redare automată, fie prin declanșarea utilizatorului. Este conștient de direcția glisorului, oferă buclă, astfel încât când ajungeți la ultimul diapozitiv, veți putea continua la primul diapozitiv. Iată cum l-am codificat:

 const changeSlide = function (direction) { slider.idle = false; slider.hero.classList.remove('prev', 'next'); if (direction == 'next') { slider.activeIndex = (slider.activeIndex + 1) % slider.total; slider.hero.classList.add('next'); } else { slider.activeIndex = (slider.activeIndex - 1 + slider.total) % slider.total; slider.hero.classList.add('prev'); } //reset classes utils().removeClasses(slider.items, ['prev', 'active']); //set prev const prevItems = [...slider.items] .filter(item => { let prevIndex; if (slider.hero.classList.contains('prev')) { prevIndex = slider.activeIndex == slider.total - 1 ? 0 : slider.activeIndex + 1; } else { prevIndex = slider.activeIndex == 0 ? slider.total - 1 : slider.activeIndex - 1; } return item.dataset.index == prevIndex; }); //set active const activeItems = [...slider.items] .filter(item => { return item.dataset.index == slider.activeIndex; }); utils().addClasses(prevItems, ['prev']); utils().addClasses(activeItems, ['active']); setCurrent(); const activeImageItem = slider.main.querySelector('.active'); activeImageItem.addEventListener('transitionend', waitForIdle, { once: true }); }

Ideea este de a determina slide-ul activ pe baza indexului său de date pe care l-am obținut din HTML. Să ne referim la fiecare pas:

  1. Setați indicatorul de inactivitate al glisorului la fals. Acest lucru indică faptul că schimbarea diapozitivelor este în desfășurare, iar gesturile de rotiță și de atingere sunt dezactivate.
  2. Clasa CSS anterioară a direcției glisorului este resetată și o verificăm pe cea nouă. Parametrul de direcție este furnizat fie implicit ca „next” dacă venim de la funcția de redare automată, fie de o funcție invocată de utilizator wheelControl sau touchControl .
  3. Pe baza direcției, calculăm indicele de diapozitiv activ și oferim cursorului clasa CSS de direcție curentă. Această clasă CSS este utilizată pentru a determina ce efect de tranziție va fi utilizat (de exemplu, de la dreapta la stânga sau de la stânga la dreapta)
  4. Slide-urile își resetează clasele CSS „de stare” (anterior, activ) folosind o altă funcție de utilitate care elimină clasele CSS, dar poate fi invocată pe o Lista Node, mai degrabă decât doar un singur element DOM. După aceea, numai diapozitivele anterioare și active în prezent li se adaugă acele clase CSS. Acest lucru permite CSS să vizeze numai acele diapozitive și să ofere o tranziție adecvată.
  5. setCurrent este un callback care actualizează indicatorul glisor pe baza ActiveIndex.
  6. În cele din urmă, așteptăm să se încheie tranziția diapozitivului de imagine activ pentru a declanșa apelul waitForIdle care repornește redarea automată dacă a fost întreruptă anterior de utilizator.

Comenzi utilizator

Pe baza dimensiunii ecranului, am adăugat două tipuri de comenzi de utilizator: rotiță și atingere. Control roti:

 const wheelControl = function () { slider.hero.addEventListener('wheel', e => { if (slider.idle) { const direction = e.deltaY > 0 ? 'next' : 'prev'; stopAutoplay(); changeSlide(direction); } }); }

Aici, ascultăm roata chiar și dacă glisorul este în mod curent în mod inactiv (nu animă în prezent o schimbare de diapozitiv) determinăm direcția roții, invocăm stopAutoplay pentru a opri funcția de redare automată dacă este în desfășurare și schimbăm diapozitivul în funcție de direcție. Funcția stopAutoplay nu este altceva decât o funcție simplă care setează indicatorul nostru de redare automată la valoarea falsă și anulează intervalul nostru invocând funcția de utilitate cancelRequestInterval , trecându-i mânerul corespunzător:

 const stopAutoplay = function () { slider.autoplay = false; utils().clearRequestInterval(slider.handle); }

Similar cu wheelControl , avem touchControl care se ocupă de gesturile tactile:

 const touchControl = function () { const touchStart = function (e) { slider.ts = parseInt(e.changedTouches[0].clientX); window.scrollTop = 0; } const touchMove = function (e) { slider.tm = parseInt(e.changedTouches[0].clientX); const delta = slider.tm - slider.ts; window.scrollTop = 0; if (slider.idle) { const direction = delta < 0 ? 'next' : 'prev'; stopAutoplay(); changeSlide(direction); } } slider.hero.addEventListener('touchstart', touchStart); slider.hero.addEventListener('touchmove', touchMove); }

Ascultăm două evenimente: touchstart și touchmove . Apoi, calculăm diferența. Dacă returnează o valoare negativă, trecem la următorul diapozitiv, deoarece utilizatorul a trecut de la dreapta la stânga. Pe de altă parte, dacă valoarea este pozitivă, ceea ce înseamnă că utilizatorul a trecut de la stânga la dreapta, declanșăm slideChange cu direcția transmisă ca „anterior”. În ambele cazuri, funcționalitatea de redare automată este oprită.

Aceasta este o implementare destul de simplă a gesturilor utilizatorului. Pentru a construi pe acest lucru, am putea adăuga butoane anterior/următorul pentru a declanșa slideChange la clic sau adăugați o listă cu marcatori pentru a merge direct la un diapozitiv pe baza indexului acestuia.

Încheiere și gânduri finale despre CSS

Așadar, iată, un mod pur CSS/JS de a codifica un aspect non-standard de glisare cu efecte de tranziție moderne.

Sper că veți găsi această abordare utilă ca mod de a gândi și că ați putea folosi ceva similar în proiectele dvs. front-end atunci când codificați pentru un proiect care nu a fost neapărat conceput convențional.

Pentru cei dintre voi interesați de efectul de tranziție a imaginii, voi trece peste acest lucru în următoarele rânduri.

Dacă revedem structura HTML a diapozitivelor pe care am furnizat-o în secțiunea de introducere, vom vedea că fiecare diapozitiv de imagine are un div în jurul său cu clasa CSS de abs-mask . Ceea ce face acest div este că ascunde o porțiune a imaginii vizibile cu o anumită cantitate, utilizând overflow:hidden și compensând-o într-o altă direcție decât imaginea. De exemplu, dacă ne uităm la modul în care este codificat diapozitivul anterior:

 &.prev { z-index: 5; transform: translate3d(-100%, 0, 0); transition: 1s $easing; .abs-mask { transform: translateX(80%); transition: 1s $easing; } }

Diapozitivul anterior are un offset de -100% în axa sa X, deplasându-l la stânga diapozitivului curent, cu toate acestea, div-ul interior al abs-mask este translat 80% la dreapta, oferind o fereastră de vizualizare mai îngustă. Acest lucru, în combinație cu un index z mai mare pentru diapozitivul activ are ca rezultat un fel de efect de acoperire - imaginea activă acoperă anterioară și în același timp își extinde zona vizibilă prin mișcarea măștii care oferă vizualizarea completă.