Pomysł na stworzenie efektu glitch chodził po mojej głowie od dawna. Co prawda za mgłą, ale chodził. Widziałem efekty na CSS-Tricks oraz na Tympanus, ale nie do końca mi odpowiadały. Zresztą opierają się o właściwość clip-path, która nie jest obsługiwana na Edge. Chciałem stworzyć coś...jakby to nazwać, mniej drętwego. Czy się udało, sam ocenisz. Tak więc dziś znów o filtrach w SVG. Wygląda na to, że wpadliśmy w pułapkę matrixa. Ale skoro już tu jesteśmy, to możemy bardziej obadać jego działanie. Przyjrzymy się pochodzeniu błędów matrixa. Będziemy Architektami! Albo raczej Psujami!
Kompozycja.
Żeby coś popsuć, trzeba najpierw coś zbudować. Na naszym stole projektowym będą potrzebne:
- obrazek,
- filtr feTurbulence,
- filtr feDisplacementMap,
- filtr feColorMatrix,
- filtr feBlend,
- filtr feComposite,
- filtr feOffset,
- filtr feMerge.
Jak widać wykorzystamy trochę więcej filtrów niż w poprzednich, podobnych efektach. Kilka z nich już znasz, ale przypomnę.
feTurbulence jest generatorem mapy pofałdowań. Sam nie zniekształca danych wejściowych, ale tworzy kolorowe fale 2D, które używamy głównie jako wejście dla feDisplacementMap. Ten filtr na podstawie wybranych składowych (np. R i G) wejścia tworzy pofałdowania drugiego wejścia (np. obrazu). feColorMatrix - ten filtr przekształca poszczególne składowe wejścia (w tym kanał alpha) na podstawie zadanej macierzy. Dziś będziemy go używać trzykrotnie i będzie realizował proste zadanie. Do blendowania tylko feBlend. Filtr dający efekty znane z programów graficznych czy też samego CSS. Miesza kolory dwóch wejść korzystając z zadanego trybu (screen, darken, lighten, itp.). feComposite łączy dwa źródła, a właściwie nakłada, na podstawie ustalonego operatora (trochę przypominającego operator logiczny). feMerge działa podobnie, ale możemy podać kilka źródeł, które zostaną połączone i otrzymamy wynik. Tu nie ma operacji logicznych. Pierwsze wejście stanowi najniższą warstwę, kolejne nakładane są na nią, a ostatnie leży na najwyższej warstwie. feOffset posłuży do przesuwania fragmentów obrazu.
Tytułowy efekt glitch wygląda tak jakby poszczególne składowe kolorów obrazu uderzały niezależnie w taneczne pląsy. Jeśli jedna będzie skakać w lewo, to druga w prawo, a trzecia, niezdecydowana raz tu, raz tu. Dodatkowo linie czy całe fragmenty obrazu będą przesuwać się również w poziomym kierunku jak klawisze klawiatury fortepianu pod palcami zręcznego wirtuoza.
Wspomniałem, że będą dwa warianty tego efektu. Obydwa będą opierać się na rozdzieleniu składowych R, G, B i szarpaniu ich z osobna. Zasada rozdzielania będzie identyczna, natomiast szarpanie w dwóch wersjach. Zacznijmy od rozdzielenia składowych.
Dekompozycja.
Wiemy już, że użyjemy filtra feColorMatrix. A jak? Macierz dla tego filtra ma wymiary 5x4. Dla wyodrębnienia składowej R będzie wyglądać tak:
R | G | B | A | mnożnik |
1 | 0 | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 0 |
0 | 0 | 0 | 1 | 0 |
Ostatnia kolumna to mnożnik, ale trafniejsze jest chyba określenie jej jako offset. Jeśli dla powyższej macierzy wstawimy w pierwszym wierszu, w tej kolumnie wartość 0.5, to składowa czerwona "zyska" na wartości. Po prostu do każdej wartości R każdego piksela zostanie dodane 0.5 (dla 8-bitowej składowej będzie to 127). Obraz zaczerwieni się. Jeśli dodamy 1, to cały obszar pokryje się jednolitym czerwonym kolorem.
Zakładając, że nasz piksel ma postać RGBA = 232, 117, 63, 1, w wyniku działania filtra z tą macierzą:
R | G | B | A | mnożnik | |||
R | 232 | 1 | 0 | 0 | 0 | 0 | 232 |
G | 117 | 0 | 0 | 0 | 0 | 0 | 0 |
B | 63 | 0 | 0 | 0 | 0 | 0 | 0 |
A | 1 | 0 | 0 | 0 | 1 | 0 | 1 |
otrzymamy piksel RGBA = 232, 0, 0, 1.
Jak wydzielić pozostałe składowe pewnie już wiesz. I to jest ta część wspólna dla obydwu wariantów. Teraz różnice.
Pierwsza wersja opiera się na użyciu filtra feDisplacementMap, który nałożymy na każdą z rozdzielonych składowych z osobna. Ta część będzie wyglądać tak:
<feColorMatrix type="matrix" values="1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0" x="0%" y="0%" width="100%" height="100%" in="SourceGraphic" result="matrixR" />
<feTurbulence type="fractalNoise" baseFrequency="0.001 0.25" numOctaves="12" seed="4" stitchTiles="stitch" x="-20%" y="-20%" width="140%" height="140%" result="turbulenceR" />
<feDisplacementMap in="matrixR" in2="turbulenceR" scale="20" xChannelSelector="R" yChannelSelector="B" x="-20%" y="-20%" width="140%" height="140%" result="displacementMapR" />
To co otrzymamy na tym etapie:
Mając teraz trzy poszarpane składowe, możemy złożyć je w całość za pomocą filtra feBlend:
<feBlend mode="screen" x="0%" y="0%" width="100%" height="100%" in="displacementMapR" in2="displacementMapG" result="blendRG" />
<feBlend mode="screen" x="0%" y="0%" width="100%" height="100%" in="displacementMapB" in2="blendRG" result="blendRGB" />
Najpierw składamy ze sobą powyginane składowe R i G (wejścia: displacementMapR oraz displacementMapG). Wynik (blendRG) łączymy z pofalowaną składową B (displacementB).
Wystarczy teraz dodać efekt animacji do filtra feTurbulence, aby ten w każdym kroku tworzył inną mapę pofałdowań. Podobnie jak w przypadku efektu szumu w SVG, animujemy wartość ziarna:
<animate attributeType="XML" attributeName="seed" values="1;99;54;3;27;120;11" dur=".25s" calcMode="discrete" repeatCount="indefinite" />
Cały kod:
<svg width="0" height="0">
<filter
id="glitch"
filterUnits="objectBoundingBox"
primitiveUnits="userSpaceOnUse"
color-interpolation-filters="linearRGB"
>
<!-- wydzielenie R -->
<feColorMatrix
type="matrix"
values="1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0"
in="SourceGraphic"
result="matrixR"
/>
<!-- fale dla R -->
<feTurbulence
type="fractalNoise"
baseFrequency="0.001 0.25"
numOctaves="12"
stitchTiles="stitch"
result="turbulenceR"
>
<animate
attributeType="XML"
attributeName="seed"
values="1;99;54;3;27;120;11"
dur=".25s"
calcMode="discrete"
repeatCount="indefinite"
/>
</feTurbulence>
<!-- pofałdowanie R -->
<feDisplacementMap
in="matrixR"
in2="turbulenceR"
scale="20"
xChannelSelector="R"
yChannelSelector="B"
result="displacementMapR"
/>
<!-- wydzielenie G -->
<feColorMatrix
type="matrix"
values="0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0"
in="SourceGraphic"
result="matrixG"
/>
<!-- fale dla G -->
<feTurbulence
type="fractalNoise"
baseFrequency="0.001 0.25"
numOctaves="12"
stitchTiles="stitch"
result="turbulenceG"
>
<animate
attributeType="XML"
attributeName="seed"
values="99;54;3;27;120;11;1"
dur=".25s"
calcMode="discrete"
repeatCount="indefinite"
/>
</feTurbulence>
<!-- pofałdowanie G -->
<feDisplacementMap
in="matrixG"
in2="turbulenceG"
scale="20"
xChannelSelector="R"
yChannelSelector="B"
result="displacementMapG"
/>
<!-- wydzielenie B -->
<feColorMatrix
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0"
in="SourceGraphic"
result="matrixB"
/>
<!-- fale dla B -->
<feTurbulence
type="fractalNoise"
baseFrequency="0.001 0.25"
numOctaves="12"
stitchTiles="stitch"
result="turbulenceB"
>
<animate
attributeType="XML"
attributeName="seed"
values="27;120;11;1;99;54;3"
dur=".25s"
calcMode="discrete"
repeatCount="indefinite"
/>
</feTurbulence>
<!-- pofałdowanie B -->
<feDisplacementMap
in="matrixB"
in2="turbulenceB"
scale="20"
xChannelSelector="R"
yChannelSelector="B"
result="displacementMapB"
/>
<!-- mieszamy R i G -->
<feBlend
mode="screen"
in="displacementMapR"
in2="displacementMapG"
result="blendRG"
/>
<!-- mieszamy RG i B -->
<feBlend
mode="screen"
in="displacementMapB"
in2="blendRG"
result="blendRGB"
/>
</filter>
</svg>
Na pierwszy rzut oka całość wydaje się skomplikowana, ale jeśli zerkniejsz jeszcze raz, to pozbędziesz się tego odczucia. Mamy tam trzy prawie identyczne fragmenty dla każdej składowej i na koniec ich połączenie.
Pierwszy wariant w całej okazałości (działa na hover - na moim Android działa przeraźliwie wolno):
W drugim wariancie użyjemy filtra feOffset zamiast pary feTurbulence + feDisplacementMap. Każdą składową będziemy z osobna przesuwać o losową wartość w lewo lub w prawo. Po czym je skleimy.
Dla składowej R filtr jest następujący:
<feOffset dy="0" x="0%" y="0%" width="100%" height="100%" in="matrixR" result="offsetR">
<animate attributeType="XML" attributeName="dx" values="20;-13;-3;6;-5;10" dur=".15s" calcMode="discrete" repeatCount="indefinite" />
</feOffset>
Trzeba pamiętać, że przesuwamy tu całość obrazu (akurat tu pozbawionego składowej G i B), a nie poszczególne linie, jak w przypadku filtra feDisplacementMap.
Kiedy skleimy składowe ze sobą za pomocą dwóch filtrów feBlend (jak poprzednio), otrzymamy coś co moglibyśmy już pozostawić na tym etapie:
Poszarpmy jednak obraz jeszcze bardziej. Moglibyśmy na całość nałożyć filtr feDisplacementMap, ale efekt przypominałby ten z pierwszego wariantu. Użyjemy więc ponownie filtra feOffset, ale tym razem podzielimy obraz na cztery poziome paski i będziemy je przesuwać niezależnie. Interesujące nas atrybuty tego filtra to:
- dx - przesunięcie w poziomie (jest też dy),
- y - start obszaru przesunięcie (analogicznie istnieje x),
- height - wysokość obszaru przesunięcia (mamy też width).
Podział będzie losowy - jeśli pierwszy pasek o wysokości 10% wytniemy od 0%, to kolejny o wysokości 30% wytniemy od 10%, itd. Zmieniając teraz wysokość pierwszego paska, kolejny pasek zacznie się na innej wysokości i jeśli jemu też nadamy losową wysokość, to kolejne będą zależne od niego. Będziemy mieć więc cztery paski o różnych wysokościach, które będziemy przesuwać losowo w lewo lub prawo.
Zobaczmy jak wygląda kod dla dwóch pierwszych pasków:
<feOffset x="0%" y="0%" width="100%" height="100%" in="glitch" result="glitch1">
<animate attributeType="XML" attributeName="dx" values="-10;5;3" dur=".25s" calcMode="discrete" repeatCount="indefinite" />
<animate attributeType="XML" attributeName="y" values="0%;0%;0%" dur=".25s" calcMode="discrete" repeatCount="indefinite" />
<animate attributeType="XML" attributeName="height" values="20%;5%;5%" dur=".25s" calcMode="discrete" repeatCount="indefinite" />
</feOffset>
<feOffset x="0%" y="0%" width="100%" height="100%" in="glitch" result="glitch2">
<animate attributeType="XML" attributeName="dx" values="12;3;-3" dur=".25s" calcMode="discrete" repeatCount="indefinite" />
<animate attributeType="XML" attributeName="y" values="20%;5%;5%" dur=".25s" calcMode="discrete" repeatCount="indefinite" />
<animate attributeType="XML" attributeName="height" values="40%;30%;15%" dur=".25s" calcMode="discrete" repeatCount="indefinite" />
</feOffset>
Za wejście dla każdego filtra służą poprzesuwane i sklejone składowe. W każdym z nich mamy trzy animacje. Pierwsza animuje przesunięcie paska, druga obszar. Pamiętaj o trzech rzeczach w tym przypadku. Wartości dla y kolejnego filtra muszą być takie jak wartości height poprzedniego filtra, czasy animacji y i height równe i takie same dla czterech filtrów, czas animacji wartości dx może się różnić od czasów animacji obszarów i może być różny dla każdego z filtrów feOffset. Czyli wysokości pasków musimy ze sobą zsynchronizować w czasie, ale przesunięcia już niekoniecznie.
Jeśli teraz chcemy połączyć cztery pocięte obszary (oczywiście możesz ich stworzyć więcej), to używamy filtra feMerge:
<feMerge x="0%" y="0%" width="100%" height="100%" result="merge">
<feMergeNode in="glitch1" />
<feMergeNode in="glitch2" />
<feMergeNode in="glitch3" />
<feMergeNode in="glitch4" />
</feMerge>
Ostatecznie uzyskamy:
<svg width="100%" height="100%">
<filter
id="glitch"
x="0%"
y="0%"
width="100%"
height="100%"
filterUnits="objectBoundingBox"
primitiveUnits="userSpaceOnUse"
color-interpolation-filters="linearRGB"
>
<!-- składowa R -->
<feColorMatrix
type="matrix"
values="1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0"
x="0%"
y="0%"
width="100%"
height="100%"
in="SourceGraphic"
result="matrixR"
/>
<!-- przesuwanie składowej R -->
<feOffset x="0%" y="0%" width="100%" height="100%" in="matrixR" result="offsetR">
<animate
attributeType="XML"
attributeName="dx"
values="20;-13;-3;6;-5;10"
dur=".15s"
calcMode="discrete"
repeatCount="indefinite"
/>
</feOffset>
<!-- składowa G -->
<feColorMatrix
type="matrix"
values="0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0"
x="0%"
y="0%"
width="100%"
height="100%"
in="SourceGraphic"
result="matrixG"
/>
<!-- przesuwanie składowej G -->
<feOffset x="0%" y="0%" width="100%" height="100%" in="matrixG" result="offsetG">
<animate
attributeType="XML"
attributeName="dx"
values="-10;5;20;-3;-30;10"
dur=".15s"
calcMode="discrete"
repeatCount="indefinite"
/>
</feOffset>
<!-- składowa B -->
<feColorMatrix
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0"
x="0%"
y="0%"
width="100%"
height="100%"
in="SourceGraphic"
result="matrixB"
/>
<!-- przesuwanie składowej B -->
<feOffset x="0%" y="0%" width="100%" height="100%" in="matrixB" result="offsetB">
<animate
attributeType="XML"
attributeName="dx"
values="20;15;13;-7;-20;5"
dur=".15s"
calcMode="discrete"
repeatCount="indefinite"
/>
</feOffset>
<!-- łączymy R i G -->
<feBlend
mode="screen"
x="0%"
y="0%"
width="100%"
height="100%"
in="offsetR"
in2="offsetG"
result="blendRG"
/>
<!-- łączymy RG oraz B -->
<feBlend
mode="screen"
x="0%"
y="0%"
width="100%"
height="100%"
in="offsetB"
in2="blendRG"
result="glitch"
/>
<!-- pierwszy wycinek i jego animacja -->
<feOffset x="0%" y="0%" width="100%" height="100%" in="glitch" result="glitch1">
<animate
attributeType="XML"
attributeName="dx"
values="-10;5;3"
dur=".25s"
calcMode="discrete"
repeatCount="indefinite"
/>
<animate
attributeType="XML"
attributeName="y"
values="0%;0%;0%"
dur=".25s"
calcMode="discrete"
repeatCount="indefinite"
/>
<animate
attributeType="XML"
attributeName="height"
values="20%;5%;5%"
dur=".25s"
calcMode="discrete"
repeatCount="indefinite"
/>
</feOffset>
<!-- drugi wycinek i jego animacja -->
<feOffset x="0%" y="0%" width="100%" height="100%" in="glitch" result="glitch2">
<animate
attributeType="XML"
attributeName="dx"
values="12;3;-3"
dur=".25s"
calcMode="discrete"
repeatCount="indefinite"
/>
<animate
attributeType="XML"
attributeName="y"
values="20%;5%;5%"
dur=".25s"
calcMode="discrete"
repeatCount="indefinite"
/>
<animate
attributeType="XML"
attributeName="height"
values="40%;30%;15%"
dur=".25s"
calcMode="discrete"
repeatCount="indefinite"
/>
</feOffset>
<!-- trzeci wycinek i jego animacja -->
<feOffset x="0%" y="0%" width="100%" height="100%" in="glitch" result="glitch3">
<animate
attributeType="XML"
attributeName="dx"
values="-5;0;4"
dur=".25s"
calcMode="discrete"
repeatCount="indefinite"
/>
<animate
attributeType="XML"
attributeName="y"
values="60%;35%;20%"
dur=".25s"
calcMode="discrete"
repeatCount="indefinite"
/>
<animate
attributeType="XML"
attributeName="height"
values="10%;20%;15%"
dur=".25s"
calcMode="discrete"
repeatCount="indefinite"
/>
</feOffset>
<!-- czwarty wycinek i jego animacja -->
<feOffset x="0%" y="0%" width="100%" height="100%" in="glitch" result="glitch4">
<animate
attributeType="XML"
attributeName="dx"
values="3;4;0"
dur=".25s"
calcMode="discrete"
repeatCount="indefinite"
/>
<animate
attributeType="XML"
attributeName="y"
values="70%;55%;35%"
dur=".25s"
calcMode="discrete"
repeatCount="indefinite"
/>
<animate
attributeType="XML"
attributeName="height"
values="30%;45%;65%"
dur=".25s"
calcMode="discrete"
repeatCount="indefinite"
/>
</feOffset>
<!-- łączymy poprzesuwane wycinki w całość -->
<feMerge x="0%" y="0%" width="100%" height="100%" result="merge">
<feMergeNode in="glitch1" />
<feMergeNode in="glitch2" />
<feMergeNode in="glitch3" />
<feMergeNode in="glitch4" />
</feMerge>
</filter>
</svg>
I teraz jeszcze dziwna ciekawostka. SVG dla tego filtra ma w przeciwieństwie do poprzedniego ustawioną szerokość i wysokość. Wartości względne powinny odpowiadać wymiarom obrazka! Bez tego efekt nie działa, a dokładnie nie działa część związana z czterema fragmentami. Druga dziwność, to potrzeba wymuszania odmalowania obrazka w CSS. Bez tego filtr zatrzyma się w jednym kroku. Inne rozwiązanie, to wstawienie do SVG elementu feImage i do niego również dodanie filtra - feImage może mieć wymiary 1x1.
Skoro SVG w tym przypadku zajmuje jakąś przestrzeń, to rozwiązanie jest takie:
<div>
<svg width="100%" height="100%">...</svg>
<img/>
</div>
+
/* nasz kontener */
div {
position: relative;
}
/* obrazek */
img {
filter: url(#filter);
animation: filter 0.03s infinite;
overflow: hidden;
position: relative;
display: block;
}
/* tej animacji używamy
tylko do wymuszenia
odmalowania obrazka,
aby animacja filtra
działała
*/
@keyframes filter {
0% {
font-size: 0;
}
1% {
font-size: 1;
}
}
/* <svg> ustawiamy pod
obrazkiem i pozycjonujemy
absolutnie. Wymiary
dopasują się do <div>,
który jest rozepchnięty
przez <img>
*/
svg {
position: absolute;
left: 0;
top: 0;
z-index: -1;
}
Wygląda to tak (również działa na hover):
Niestety nie potrafię Ci obiecać, że w kolejnym wpisie wydostaniemy się z SVG'owego matrixa. Mam nadzieję, że jednak wpadniesz. Do przeczytania!
Przydatne linki:
Efekt glitch z Tympanus.
Efekt glitch z CSS-Tricks.
Efekt glitch wariant 1.
Efekt glitch wariant 2.