Images czyli grafika na stronie

Przypuśćmy, że mamy obrazek img na stronie:

Kartofel w wietnamie


    <img src="./wietnam.jpg" class="img" alt="Kartofel w wietnamie" width="400" height="400">

Żeby móc na nim pracować, pobierzmy go i zbadajmy w konsoli (zrób to teraz):


const img = document.querySelector('.img');
console.dir(img);
Widok części debugera po zbadaniu grafiki

Jak widzisz w konsoli debugera (mam nadzieję, że masz ją otwartą), dla tak pobranej grafiki możemy ustawiać mnóstwo właściwości i korzystać z wielu metod.

Wśród nich jednymi z najczęściej używanymi są:

width szerokośc grafiki,
height wysokość grafiki,
alt alternatywny opis grafiki (widoczny gdy się nie wczyta),
title tekst, który pojawi się po najechaniu kursorem na element,
src adres do grafiki

const obr = document.querySelector('.img');
console.log('Szerokosc: ' + img.width + ', wysokosc: ' + img.height + ', src:' + img.src);

No dobrze - w praktyce używa się tego co pasuje w danej sytuacji. Poniżej zajmiemy się częstymi przypadkami, które są spotykane przy pracy z grafiką w js.

Efekt rollover

Efekt rollover, czyli podmiana grafiki po wskazaniu jej kursorem.

Do efektu rollover powinien być stosowany zwykły CSS i pseudoselektor :hover.
Jeżeli nie masz powodu by wykorzystywać do tego JS - nie rób tego.

Aby podmienić grafikę na inną musimy zmienić atrybut src danej grafiki:


const obr = document.getElementById('obrazek');

img.addEventListener('mouseover', function() {
    this.src = 'obrazek_2.jpg';
});

img.addEventListener('mouseout', function() {
    this.src = 'obrazek_1.jpg';
});

Powyższa metoda ma jeden błąd. Nowa grafika jest ściągana dopiero, gdy wskażemy nasz obrazek kursorem. Przy wolnych łączach lub dużych grafikach spowoduje to "dziurę", bo obrazek będzie wczytywany dopiero po najechaniu.
Aby temu zapobiec musimy wszystkie grafiki biorące udział w efekcie rollover załadować zawczasu.

Aby załadować wcześniej grafiki możemy skorzystać z różnych sztuczek. Przykładowo możemy wrzucić na stronę obrazek z daną grafiką. Obrazek taki za pomocą pozycjonowania absolutnego wynosimy poza ekran (np. position:absolute; left:-9999px). Grafikę taką moglibyśmy też dać jako tło jakiegoś ukrytego elementu - tutaj raczej szedł bym w visibility:hidden, height:0; width:0;, bo nie jest powiedziane, że tło elementu ukrytego za pomocą display:none zawsze będzie dociągany.

Możemy też zastosować zupełnie inne podejście polegające na stworzeniu obrazka za pomocą JS.
W tym celu możemy stworzyć obrazek za pomocą document.createElement('img') lub korzystając z konstruktora Image().


const newImage = new Image(200, 300); //przy tworzeniu Image możemy podać jego rozmiary
newImage.src = "obrazek_on.jpg" //podajemy jego src

document.querySelector('body').appendChild(newImage);

const newImage = document.createElement('img');
newImage.width = 200;
newImage.height = 300;
newImage.src = "obrazek_on.jpg" //podajemy jego src

document.querySelector('body').appendChild(newImage);

Gdy utworzymy już stosowne obiekty, możemy odwoływać się do ich właściwości src. Poniższy skrypt realizuje całe zadanie:


const imageOff = new Image();
imageOff.src = '/images/obrazek1.jpg';

const imageOn = new Image();
imageOn.src = '/images/obrazek2.jpg';


const img = document.querySelector('.pictureOnPage');

img.addEventListener('mouseover', function() {
    this.src = imageOn.src;
});
img.addEventListener('mouseout', function() {
    this.src = imageOff.src;
});

Oczywiście nie tylko zdarzeniem mouseover człowiek żyje.
Można przecież także i click wykorzystać:


<img src="/images/karto_1.jpg" title="Kliknij!" id="obrazekKarto" />
<span id="zastrzel_tekst">Zastrzel kartofla!</span>

const imageKill = new Image();
imageKill.src = 'karto_2.jpg';

const img = document.querySelector('#obrazekKarto');
img.addEventListener('click', function() {
    this.src = imageKill.src;
    document.querySelector('#shootText').innerHTML = 'I Ty przeciwko Fantomasowi?'
});


Zastrzel kartofla!

Efekt rollower dla większej ilości obrazków

Deklarowanie dla każdej grafiki obu stanów w podany powyżej sposób może być problematyczne - zwłasza dla większej liczby grafik.
O wiele lepszym rozwiązaniem jest zastosowanie tablicy do przechowywania nazw obrazków, a następnie za pomocą pętli dynamiczne tworzenie obiektów Image, które będziemy trzymać w dodatkowej tablicy:


const names = [
    'obrazek.jpg',
    'kartofelek.jpg',
    'piesek.jpg',
    'kotek.jpg',
    'czekolada.jpg'
];
const images = [];

for (let i=0; i<names.length; i++) {
    const images[i] = new Image();
    images[i].src = names[i];
}

Load

Obiekty wczytywane (np. window, image, script itp.) posiadają zdarzenie load, które wykrywa, czy dany obiekt został w pełni załadowany.

Dla obiektu window zdarzenie to oznacza wczytanie całego dokument dom oraz wszystkich grafik (w przeciwieństwie do DOMContentLoaded, które odpalane jest po wczytaniu drzewa DOM, ale przed wczytaniem grafik).
Dla grafiki oznacza to wczytanie grafiki:


img.addEventListener('load', function() {
    console.log('Dana grafika została załadowana');
});

img.src = 'lorem.jpg'; //wpierw ustawiamy zdarzenie, potem ustawiamy src

Jeżeli w powyższym skrypcie zdarzenie load ustawilibyśmy po ustawieniu src, skrypt mógł by zadziałać nieprawidłowo. Pamiętaj - event load zaczyna działać, gdy zakończy się wczytywanie jakiegoś zasobu. Może zdarzyć się sytuacja, że obrazek wczyta się natychmiastowo po ustawieniu src (np. siedzi już w cache przeglądarki). Oznacza to, że obrazek wczyta się przed odpaleniem nasłuchiwania eventu load.

Dlatego właśnie ważna jest kolejność w powyższym skrypcie. Najpierw podpinamy load, potem ustawiamy src.

Są jednak sytuacje (szczególnie przy bardziej skomplikowanych skryptach), że nie jesteśmy w stanie zagwarantowac takiej kolejności. Przykładowo system podpina wszystkie eventy, a gdzieś dalej dopiero budowane są src grafik w systemie. W takim przypadku warto sprawdzić właściwość complete każdej grafiki, która wskazuje czy dany obrazek jest wczytany czy nie.


img.src = 'lorem.jpg';

img.addEventListener('load', function() {
    console.log('Dana grafika została załadowana');
});

if (img.complete) {
    //odpalam ręcznie event load
    const event = new Event('load');
    img.dispatchEvent(event);
}

Postęp wczytywania

Nazwy obrazków do załadowania podajemy w formie tablicy. Następnie wykonujemy pętlę po tej tablicy, tworząc obiekty Image z odpowiednim src. Dla każdego obiektu definiujemy zdarzenie load. Będzie ono wywoływało funkcję, która sprawdza ile obiektów zostało już załadowanych i odpowiednio ustawiało długość paska ładowania (w procentach). Aby móc sprawdzać ile obiektów zostało załadowanych, musimy posłużyć się dodatkową zmienną ile_zaladowano.


.loading-bg {
    margin:30px 0;
    width:600px;
    height:40px;
    background:#eee;
    border:1px solid #ddd;
    overflow:hidden;
}
.loading-bg .progress {
    width:0;
    height:100%;
    background:#EC185D;
    overflow:hidden
}

    <div id="progressCnt"></div>

//tablica z nazwami obrazków do załadowania
const imgNames = [
    'obrazek1.gif',
    'obrazek2.gif',
    'obrazek3.gif',
    'obrazek4.gif',
    'obrazek5.gif',
    'obrazek6.gif'
];
let howLoaded = 0; //ile obiektów Images już załadowano do pamięci
const loadingStep = (100 / imgNames.length); //szerokość oznaczająca % paska po załadowaniu 1 obrazka
const images = []; //tablica będzie zawierała obiekty Image
let loadingBarBg = null; //zmienna pod którą utworzymy dynamicznie div zawierającego div-pasek postępu
let loadingBar = null; //zmienna pod którą utworzymy dynamicznie div-pasek postępu
const pageToRedirect = 'index.php'; //strona na którą przeniesie po zakończonym ładowaniu

//funkcja odpalana dla każdego obiektu Image, które wcześniej stworzyliśmy.
//Sprawdza ile obiektów zostało załadowanych i ustawia odpowiednią szerokość paska.
function setLoadingBar() {
    howLoaded++;
    loadingBar.style.width = howLoaded * loadingStep + "%"; //zmianiamy szerokość paska (podaną w %)

    if (howLoaded >= imgNames.length) {
        setTimeout(function() {
            location.href = pageToRedirect; //po załadowaniu wszystkich grafik czekamy 2s i przenosimy na stronę
        }, 1000)
    }
}

//funkcja rozpoczynająca ładowanie obrazków
function startLoading() {
    const div = document.querySelector('#progressCnt');

    loadingBarBg = document.createElement('div');
    loadingBarBg.className = 'loading-bg';    //dzięki temu skorzystamy ze zdefiniowanych styli

    loadingBar = document.createElement('div');
    loadingBar.className = 'progress';

    loadingBarBg.appendChild(loadingBar);

    div.appendChild(loadingBarBg);

    for (let i=0; x<imgNames.length; i++) { //pętla po nazwach obrazków...
        images[i] = new Image();
        images[i].addEventListener('load', setLoadingBar.bind(this));    //dla każdego obiektu ustawiamy zdarzenie onload
        images[i].src = imgNames[i];
    }
}

document.addEventListener("DOMContentLoaded", function(event) {
    startLoading();
});

Powyższy przykład możesz zobaczyć w działaniu tutaj.

Wczytywanie grafiki i promises

Do wczytywania wielu grafik możemy też skorzystać z Promises, które pojawiły się w ES6.

Obietnice udostępniają nam metodę all(), która odpalana jest, gdy wszystkie przekazane do niej promisy zostaną rozwiązane (wykonane). Takim promisem może być funkcja asynchroniczna, jakiś setTimeout, ale też moment, gdy grafika się wczyta.

Napiszmy prostą funkcję, która zwróci nam promise, który zostanie rozwiązany gdy grafika zostanie wczytana. W przeciwnym razie zostanie zwrócony błąd. Poniższa funkcja to tak naprawdę powyżej podawane informacje okryte prostym Promise:


const loadImage = function(src) {
        return new Promise((resolve, reject) => {
            const img = new Image();

            img.onload = () => {
                resolve(img);
            };
            img.onerror = (e) => reject(e);
            img.src = src;

            if (img.complete) {
                const loadEvent = document.createEvent('Event');
                loadEvent.initEvent('load', false, false);
                img.dispatchEvent(loadEvent);
            }
        });
}

Wykorzystajmy ją w funkcji wczytującej paczkę grafik:


const loadGraphics = function(tabGraphicSrc) {
    const loadImage = function(src) {
            return new Promise((resolve, reject) => {
            const img = new Image();

            img.onload = () => {
                resolve(img);
            };
            img.onerror = (e) => reject(e);
            img.src = src;

            if (img.complete) {
                const event = new Event('load');
                img.dispatchEvent(event);
            }
        });
    }

    //tablica z grafikami
    //każdy jej element zawierać będzie promise z wczytywaną grafiką
    const promises = [];
    tabGraphicSrc.forEach(el => {
        promises.push(loadImage(el));
    });

    return Promise.all(promises);
}

Od tej pory oczekiwanie na wczytanie wszystkich grafik będzie bardzo proste:


const imgSrc = ['grafika1.jpg', 'grafika-moja2.jpg', 'grafika-inna3.png', 'grafika4.jpg']

loadGraphic(imgSrc).then(data => {
    console.log(data);
});

Przykładowo możemy sobie stworzyć obiekt zawierający wczytane grafiki, którego klucze będą miały nazwy tych grafik bez rozszerzenia:


const imgSrc = ['grafika1.jpg', 'grafika-moja2.jpg', 'grafika-inna3.png', 'grafika4.jpg']
const images = {}; //obiekt z wczytanymi grafikami

//promisy są w ES6 - używamy więc tej wersji - czyli funkcje strzałkowe
loadGraphic(imgSrc).then(data => {
    //kluczami będą nazwy grafik - np. grafika1, grafika-moja2, grafika-inna3
    data.forEach(img =>
        const key = img.src.replace(/^.*[\\\/]/, '').replace(/\..*$/,'');
        images[key] = img;
    });

    console.log(images['grafika1']);
    console.log(images['grafika-moja2']);
    console.log(images['grafika-inna3']);
    console.log(images['grafika4']);
});

Trening czyni mistrza

Poniżej zamieszczam kilka zadań, które w ramach ćwiczenia możesz wykonać:

  1. Utwórz jeden obiekt typu Image. Ustaw mu src do jakiejś grafiki. Następnie stwórz za pomocą JS div o rozmiarach np. 400x300, któremu jako tło ustawisz tą grafikę. Grafika tła powinna być dopasowana do wielkości diva.
    
                    const img = new Image();
                    img.src = "https://placeimg.com/640/480/animals?t=1514540372818";
    
                    const div = document.createElement('div');
                    div.style.setProperty('width', 400);
                    div.style.setProperty('height', 300);
                    div.style.setProperty('background-image', img.src);
                    div.style.setProperty('backgrouns-size', 'cover'); //lub contain jeżeli ma się całe mieścić w divie
    
                    document.querySelector('body').appendChild(div);
                
  2. Po kliknięciu na div, zmień mu grafikę na inną (którą pobierzesz z kolejnego Image)
    
                const img = new Image();
                img.src = "https://placeimg.com/640/480/animals?t=1514540372818";
    
                const img2 = new Image();
                img2.src = "https://placeimg.com/640/480/animals?t=1514540470097";
    
                const div = document.createElement('div');
                div.style.setProperty('width', 400);
                div.style.setProperty('height', 300);
                div.style.setProperty('background-image', img.src);
                div.style.setProperty('backgrouns-size', 'cover');
    
                document.querySelector('body').appendChild(div);
    
                div.addEventListener('click', function() {
                    this.style.setProperty('background-image', img2.src);
                });
            
  3. Dla chętnych. Na środku powyżej stworzonego postępu ładowania (w artykule) dodaj tekst, który będzie pokazywał liczbę wczytywanych obrazków w formacie "Wczytano 5 / 10"
  4. Pobierz sobie stronę zadanie-obrazki.html (prawym klawiszem myszy na link i zapisz jako). W kodzie strony masz kilka obrazków.

    Twoim celem jest:
    - pobrać z nich atrybut src i podstawić go pod atrybut data
    - dodać dla img klasę .loading
    - poczekać aż wczyta się grafika, wtedy usunąć .loading i przywrócić na nowo src z grafiką.

    Aby móc testować to zadanie w debugerze w zakładce Network na górze możesz sobie ustawić testową szybkość połączenia. Dodatkowo odświeżaj stronę poprzez Ctrl + Shift + R (odświeżenie z pominięciem cache).
  5. Dla chętnych. Powyższe zadanie zmodyfikuj tak, by grafiki zaczęły się ładować dopiero, gdy dany obrazek znajdzie się na ekranie. Możesz skorzystać z https://stackoverflow.com/questions/5353934/check-if-element-is-visible-on-screen