chevron-left chevron-right

[CSS] Wizualizacja ekranu meczowego Premier League za pomocą HTML i CSS

Jestem wielkim fanem piłki nożnej. Bardzo często zdarza mi się je oglądać. Jedną z lig, które oglądam jest liga angielska.

W poprzednim sezonie pojawiła się nowa wizualizacja ekranów meczowych i od momentu jak je zobaczyłem, to zapragnąłem odtworzyć podobny efekt. Dla tych, którzy nie są obeznani w transmisjach z ligi angielskiej poniżej zamieściłem nagranie z przykładowego meczu.

premier-league-screens-on-TV

Jak widać, animacje polegają na przesuwaniu się paneli w odpowiednim czasie. Efekt wydaje się nieskomplikowany do zbudowania za pomocą HTML i CSS.

Struktura HTML projektu

Praktycznie każdy projekt, który prezentuję na łamach tego bloga zaczyna się od przedstawienia struktury HTML. Ma to na celu zobrazowanie ilości elementów, które okażą się przydatne w celu osiągnięcia zamierzonej animacji. W przypadku projektu z tego wpisu kod HTML wygląda następująco:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<div class="intro">
    <div class="intro__panel--left"></div>
    <div class="intro__panel--right">
        <div class="intro__league-badge-wrapper">
            <div class="intro__league-badge-outer">
                <img src="./premier-league-logo.png" alt="Premier League Badge" class="intro__league-logo">
                <img src="./premier-league-oneline-badge.png" alt="Premier League Badge" class="intro__league-badge">
            </div>
        </div>
    </div>
    <div class="intro__teams-wrapper">
        <div class="intro__teams">
            <div class="intro__team--home">
                <div class="intro__team-name">
                    <div class="intro__field-info">Live from Anfield</div>
                    <div class="intro__team-name-label">Liverpool</div>
                </div>
                <div class="intro__team-badge-wrapper">
                    <img src="./liverpool-badge.png" alt="Liverpool FC badge" class="intro__team-badge">
                </div>
            </div>
            <div class="intro__team--away">
                <div class="intro__team-name">Hull City</div>
                <div class="intro__team-badge-wrapper">
                    <img src="./hull-city-badge.png" alt="Hull City AFC badge" class="intro__team-badge">
                </div>
            </div>
            <div class="intro__hashtag">#LIVvHUL</div>
        </div>
    </div>
</div>

Kod HTML został podzielony na 3 sekcje: 2 panele i część w której znajdują się informacje o meczu. Analizując powyższy kod, można zobaczyć, że element o klasie .intro__field-info został umieszczony wewnątrz elementu z klasą .intro__team-name. Jest to celowe działanie, gdyż będziemy chcieli pozycjonować informacje o miejscu odbywania się meczu względem elementu z nazwą drużyny.

Inną rzeczą wartą uwagi jest umiejscowienie dwóch wersji loga rozgrywek. Obydwa loga zostały umieszczone obok siebie w jednym elemencie nadrzędnym. Dzięki temu, o wiele łatwiej będzie zreprodukować animację przejścia między wersjami loga rozgrywek.

Kod CSS widoku z logiem ligi

Efekt posiada dwa stany/widoki: widok z logiem rozgrywek na pełnym ekranie i widok z zespołami i informacjami o meczu. W tej części skupimy się na wyświetleniu pierwszego widoku. Logo wraz z nazwą rozgrywek będzie wyświetlone na środku panelu, który będzie wypełniał całą szerokość ekranu. Kod wygląda następująco:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
@font-face {
    font-family: 'PremierSans-Regular';
    src: url('./fonts/PremierLeagueW01-Regular.woff2') format('woff2');
}
 
@font-face {
    font-family: 'PremierSans-Bold';
    src: url('./fonts/PremierLeagueW01-Bold.woff2') format('woff2');
}
 
@font-face {
    font-family: 'PremierSans-Light';
    src: url('./fonts/PremierLeagueW01-Light.woff2') format('woff2');
}
 
* {
    box-sizing: border-box;
}
 
body {
    font-family: 'PremierSans-Regular', Roboto, Helvetica, Arial, sans-serif;
    background: url('anfield.jpeg');
    background-size: cover;
    margin: 0;
    padding: 0;
}
 
.intro {
    display: flex;
    height: 80vh;
    width: 100%;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate3d(-50%, -50%, 0);
    overflow: hidden;
}
 
[class^="intro__panel"] {
    background: rgba(37, 215, 224, .8);
    transition: all .5s cubic-bezier(0.785, 0.135, 0.150, 0.860);
}
 
.intro__panel--right {
    width: 100%;
}
 
.intro__league-badge-wrapper {
    position: relative;
    height: 100%;
}
 
.intro__league-badge-outer,
.intro__league-logo {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate3d(-50%, -50%, 0);
}
 
.intro__league-badge {
    width: 640px;
    height: 200px;
}
 
.intro__league-logo {
    height: 108px;
    width: auto;
    opacity: 0;
}
 
.intro__league-logo,
.intro__league-badge {
    transition: opacity .5s ease-out;
}

W powyższym kodzie warto zwrócić uwagę na kilka rzeczy. Po pierwsze, zamiast deklarować wszystkie możliwe typy fontów dla wszystkich przeglądarek, zdefiniowałem tylko jeden typ Woff2, który jest wspierany przez najnowsze wersje przeglądarek. Docelowo, ten typ czcionki jest najbardziej optymalny w kontekście ładowania czcionek przez przeglądarkę.

Kolejną rzeczą na którą warto zwrócić uwagę jest wyświetlanie tła dla elementu <body />. Ustawiając wartość własności background-size na cover sprawiamy, że tło wypełnia całą dostępną przestrzeń. Jeśli z jakiegoś powodu tło będzie większe niż kontener do którego zostanie zaimplementowane, to tło się przeskaluje zachowując proporcje. Jeśli z jakiegoś powodu proporcje kontenera nie będą się zgadzały z proporcjami tła to wtedy zostanie ono odpowiednio ucięte.

Ostatnią rzeczą wartą uwagi jest wykorzystanie transform: translate3d(); ma ono na celu przerzucenie obliczeń animacji danego elementu do procesora graficznego.

Kod CSS widoku z drużynami

Pora zająć się widokiem odpowiedzialnym za wyświetlenie drużyn i informacji o spotkaniu. Pomiędzy widokiem z nazwą rozgrywek a widokiem z drużynami nastąpi przejście w trakcie, którego stopniowo będą się pojawiać kolejne elementy ekranu. Zobaczmy jak wygląda CSS:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
.intro__teams-wrapper {
    opacity: 0;
    position: absolute;
    top: 50%;
    left: 15vw;
    transform: translate3d(0, -50%, 0);
    width: 70vw;
    overflow: hidden;
}
 
.intro__teams {
    display: flex;
    flex-direction: column;
    width: 600px;
}
 
[class^="intro__team--"] {
    display: flex;
    position: relative;
    overflow: hidden;
}
 
.intro__team--home {
    transform: translate3d(-50vw, 0, 0);
    z-index: 3;
    transition: transform .5s .5s ease-out;
}
 
.intro__team--away {
    transform: translate3d(10vw, -100%, 0);
    opacity: 0;
    z-index: 2;
    transition: transform .5s 1s ease-out, opacity .5s 1.1s ease-out;
}
 
.intro__team-name {
    padding: 0 30px;
    line-height: 90px;
    background: rgba(64, 4, 68, 1);
    color: #fff;
    text-transform: uppercase;
    font-size: 72px;
    z-index: 2;
    box-shadow: 0 0 5px 1px rgba(0, 0, 0, .5);
    font-family: 'PremierSans-Bold', Roboto, Helvetica, Arial, sans-serif;
}
 
.intro__team--away .intro__team-name:before {
    content: '';
    flex: 1 1 100%;
    height: 0;
    width: calc(100% - 180px);
    position: absolute;
    left: -30px;
    top: 0;
    background: #000;
    box-shadow: 0 1px 5px 1px rgba(0, 0, 0, .65);
}
 
.intro__team-badge-wrapper {
    height: 90px;
    width: 90px;
    padding: 4px;
    background: #fff;
    display: flex;
    justify-content: center;
    box-shadow: 0 0 0 0 rgba(0, 0, 0, .65);
}
 
.intro__team-badge {
    max-width: 100%;
    width: auto;
    max-height: 100%;
    height: auto;
}
 
.intro__team-badge-wrapper {
    transform: translate3d(-90px, 0, 0);
    transition: transform .5s 1.7s ease-out;
}
 
.intro__hashtag {
    background: rgb(37, 215, 224);
    display: flex;
    padding: 0 30px;
    line-height: 24px;
    font-weight: 700;
    transform: translate3d(10vw, -100%, 0);
    align-self: flex-start;
    position: relative;
    z-index: 1;
    overflow: hidden;
    opacity: 0;
    transition: transform .5s 1.7s ease-out, opacity .5s 1.8s ease-out;
}
 
.intro__hashtag:before {
    content: '';
    height: 0;
    width: 100%;
    box-shadow: 0 1px 5px 1px rgba(0, 0, 0, .65);
    flex: 1 1 100%;
    position: absolute;
    top: 0;
    left: 0;
}
 
.intro__field-info {
    background: rgba(64, 4, 68, .9);
    color: rgb(37, 215, 224);
    display: flex;
    padding: 0 30px;
    line-height: 24px;
    font-family: 'PremierSans-Regular', Roboto, Helvetica, Arial, sans-serif;
    font-weight: 700;
    font-size: 16px;
    align-self: flex-end;
    transform: translate3d(-155px, 100%, 0);
    position: relative;
    z-index: 2;
    overflow: hidden;
    opacity: 0;
    position: absolute;
    top: 0;
    right: -30px;
    transition: transform .5s 1.7s ease-out, opacity .5s 1.8s ease-out;
}
 
.intro__field-info:after {
    content: '';
    height: 0px;
    width: 100%;
    box-shadow: 0 -1px 5px 1px rgba(0, 0, 0, .65);
    flex: 1 1 100%;
    position: absolute;
    bottom: 0;
    left: 0;
}
 
.intro__teams,
.intro__team-badge {
    transition: all .5s cubic-bezier(0.785, 0.135, 0.150, 0.860);
}
 
.intro--teams {
    justify-content: space-between;
}
 
.intro--teams .intro__panel--left {
    width: 30vw;
}
 
.intro--teams .intro__panel--right {
    width: 15vw;
}
 
.intro--teams .intro__teams-wrapper {
    opacity: 1;
}
 
.intro--teams .intro__team--home {
    transform: translate3d(0, 0, 0);
    padding-top: 24px;
}
 
.intro--teams .intro__team--away {
    transform: translate3d(10vw, 0, 0);
    opacity: 1;
}
 
.intro--teams .intro__league-badge {
    opacity: 0;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate3d(-50%, -50%, 0);
}
 
.intro--teams .intro__league-logo {
    opacity: 1;
}
 
.intro--teams .intro__team-badge-wrapper {
    transform: translate3d(0, 0, 0);
}
 
.intro--teams .intro__hashtag {
    opacity: 1;
    transform: translate3d(10vw, 0, 0);
}
 
.intro--teams .intro__field-info {
    opacity: 1;
    transform: translate3d(-155px, 0, 0);
}

Drugi listing kodu jest dużo dłuższy od pierwszego z tego względu, że definiuje zachowanie projektu w zależności od tego czy ekran z drużynami jest aktywny, czyli ma klasę .intro--teams. Również i tutaj jest kilka ciekawostek związanych z CSS.

Jedną z nich jest zastosowanie uciętego cieniowania elementu. Zazwyczaj gdy stosujemy zapis własności box-shadow podobny do tego: box-shadow: 0 0 5px 1px rgba(0, 0, 0, .5); to sprawiamy, że cień rozlewa się wokół elementu. W przypadku efektu prezentowanego w tym artykule, interesowało mnie aby cień znajdował się w konkretnym miejscu (czyli poniżej elementu i nigdzie indziej). W celu osiągnięcia tego efektu dołożyłem własność overflow: hidden; do selektora CSS odpowiednialnego za wybór elementu nadrzędnego w stosunku do elementu cieniowanego. W taki sposób uciąłem cień.

Inną ciekawą rzeczą, jest zastosowanie opóźnień w animacjach przejść między elementami i kolejkowanie animacji: transition: transform .5s 1.7s ease-out, opacity .5s 1.8s ease-out;. Zdecydowałem, że przejście między dwiema wartościami dla własności transform będzie trwało pół sekundy po odczekaniu 1.7 sekundy, a zmiana stanu przezroczystości elementu nastąpi po 1.8 sekundy i również będzie trwała pół sekundy. Jak widać kolejkowanie zmian między wartościami własności jest bardzo proste. W przypadku, gdybyśmy chcieli zmieniać wartości tej samej własności w różnym momencie czasu to wtedy musielibyśmy zastosować CSS keyframes, czyli animację klatkową.

Podsumowanie

Powyższy kod był testowany w najnowszych wersjach przeglądarek Mozilla Firefox, Google Chrome i Opera. Tam działa, tak jak to sobie zaplanowałem. W innych przeglądarkach mogą wystąpić błędy.

Budując ten efekt, starałem się odwzorować jak najwierniej ekran meczowy Premier League. Zdaję sobie sprawię, że można poprawić kilka rzeczy. Między innymi timing przejść między ekranami i timing pojawiania się kolejnych elementów na ekranie (nie byłem w stanie odwzorować w 100% tego samego przejścia, jaki występuje w oryginale). Możnaby również poprawić sposób w jaki następuje przejście między dwoma wersjami loga rozgrywek.

Mam nadzieję, że dzisiejszy tekst jest dla Ciebie inspirujący i zmotywuje Ciebie do próbowania odtwarzania rzeczy, które widujesz w TV za pomocą CSS, HTML czy JavaScript. Być może dzięki temu tekstowi zdołasz się też nauczyć czegoś nowego, nowego podejścia do kodowania.

Zapraszam do komentowania i dzielenia się uwagami dotyczącymi projektu. Pliki projektu można pobrać z repozytorium na Githubie a demo można zobaczyć w tym miejscu: demo projektu Premier League.

  • U mnie pod FF przy kolejnych pętlach jest problem z Hullu (za wcześnie się pojawia – jak znika główny ekran).

  • Masz rację, ale problem występuje jeśli klikniesz w przycisk zaraz po ponownym załadowaniu pierwszego ekranu. Musisz poczekać ok. 4 sekundy aby animacja od początku działała poprawnie. Uznałem, że na potrzeby tego konceptu nie ma potrzeby martwić się o przejście zwrotne widoków.
    Dzięki za zwrócenie uwagi! 🙂

  • Szacun za chęci! To właśnie dla takich momentów cieszę się, że jestem programistą i że mogę sobie zrobić kiedy chce taki efekt. Świetna sprawa. Gratulacje Piotr!
    PS Ten gif na z początku artykułu trochę laguje, może osadzić film, który ładnie prezentuję docelową animację?

  • Blazej Orlowski

    Na safari logo wyleciało w kosmos 🙂

  • Dzięki za zwrócenie uwagi 🙂