Kiedy kilka lat temu Google wypuściło Fluttera, ich motto brzmiało bardzo prosto: Masz kontrolę nad każdym pikselem na ekranie. Co w zasadzie przekłada się na Od teraz możesz zrobić wszystko w dowolnym miejscu swojej aplikacji, włączając w to te urocze płynne menu lub animacje ścieżek, które mogłeś zrobić tylko w After Effects. Ale jak oni to niby zrobili?
Było to możliwe dzięki bibliotece Skia i silnikowi renderującemu, który omówimy w tym artykule. Dla tych, którzy nie wiedzą, Skia jest systemem renderowania akcelerowanym układami GPU, który zawiera wiele graficznych API, w tym rysowanie ścieżek, filtry obrazu, niestandardowe shadery i cały zestaw narzędzi SVG. Dlatego ludzie z Fluttera wpadli na pomysł, że robienie Skia canvas jako cały obszar wyświetlania aplikacji i każdy element GUI wewnątrz, zostałby przerysowany i wyglądałby jak natywny. Ponieważ wszystkie są w Skia canvas, to jeśli zrobiłbyś coś wymyślnego i „fancy”, przykładowo rozmywanie, to też mógłbyś to zrobić. To oczywiście błyskawicznie wywołało efekt wow w społeczności i od tego czasu ludzie tworzyli ciekawe i barwne projekty dla swojej aplikacji.
Wiemy już czym jest Skia, tak więc wróćmy do tematu naszego artykułu;
Jeśli jesteś programistą React Native, który uwielbia oglądać tutoriale, czytać wszystkie artykuły, robić wszystkie potrzebne rzeczy, aby się doskonalić, to zapewne jest szansa, że napotkałeś w swoich łowach programistę Williama Candillona, który poświęcił się animacjom i interaktywnym UI w React Native. Na swoim kanale na YouTube ma mnóstwo ambitnych tutoriali, dzięki którym opanujesz animacje w React Native lub animacje w ogóle. Oglądając jego filmy, dowiedziałem się również sporo o koncepcjach i technikach matematycznych.
Mniej więcej w zeszłym roku William i jego przyjaciel Christian Falch postanowili „przenieść” bibliotekę Skia na React Native i stworzyli wspaniałą paczkę o nazwie react-native-skia.
Ta biblioteka zawiera prawie wszystkie API biblioteki Skia wraz z technikami animacji, które mogą być znane z react-native-reanimated. Jeśli kiedykolwiek używałeś react-three-fiber do animacji Three JS, ta sama koncepcja ma zastosowanie tutaj. Każdy interfejs API Skia jest wykonywany w oddzielnym kontekście, który używa własnych hooków i wartości. Zacznijmy od przykładu, a później wyjaśnię, co mam na myśli;
import React from "react";
import { Canvas, Circle, Group } from "@shopify/react-native-skia";
export const HelloWorld = () => {
const width = 256;
const height = 256;
const r = 215;
return (
<div style={{ flex: 1 }}>
<Canvas style={{ flex: 1 }}>
<Group blendMode="multiply">
<Circle cx={r} cy={r} r={r} color="cyan" />
<Circle cx={width - r} cy={r} r={r} color="magenta" />
<Circle cx={width / 2} cy={height - r} r={r} color="yellow" />
</Group>
</Canvas>
</div>
);
};
Ten kod tworzy 3 koła z trybem multiply blend
zastosowanym na nakładających się częściach. Więc najpierw definiujemy Canvas
dla naszej farby, które w zasadzie tworzą kontekst dla naszych obiektów. Następnie definiujemy Grupę, która pozwala nam na stosowanie transformacji, przycinania i innych operacji. Grupa jest niezbędnym znacznikiem w RN Skia, ponieważ kształty i inne obiekty nie mają indywidualnych właściwości transformacji. Więc jeśli chciałbyś przenieść na przykład Rectangle, musisz najpierw owinąć go za pomocą Group
.
Szybka wrzutka:
React Native Skia ma również imperatywne API, co oznacza, że zamiast deklarować znaczniki, po prostu wykorzystujesz bibliotekę za pomocą zestawu poleceń. Nie obejmuję imperatywnego sposobu w tym artykule, ponieważ jest to trochę sprzeczne z deklaratywną naturą Reacta. Jeśli zauważysz, że masz problem z zastosowaniem swojej logiki w znacznikach, możesz przełączyć się na imperatywne API na podstawie komponentów. Przykład znajdziesz tutaj.
Przechodząc dalej do kształtów, React Native Skia ma prymitywne kształty, takie jak prostokąty, koła, wielokąty i linie, a także ścieżki zdefiniowane za pomocą znanej notacji SVG. Atrybuty kształtów są niemal identyczne jak ich odpowiedniki w SVG, na przykład Circle ma cx, cy lub Rect ma x, y i tak dalej. Jedyną rzeczą jest to, że istnieją pewne elementy dla konkretnych przypadków, jak <Box>
do optymalizacji wewnętrznych i zewnętrznych cieni lub <Patch>
do rysowania powierzchni Coonsa (nie jestem pewien jaki jest przypadek użycia tego elementu).
Kształty przyjmują jako swoje dziecko <Paint>
, aby zmodyfikować właściwości malowania, takie jak konturowanie lub wypełnienie. Spójrz na poniższy przykład:
import { Canvas, Circle, Group, Paint } from "@shopify/react-native-skia";
export const PaintDemo = () => {
const strokeWidth = 10;
const r = 128 - strokeWidth / 2;
return (
<Canvas style={{ flex: 1 }}>
<Group opacity={0.5}>
<Circle cx={r + strokeWidth / 2} cy={r} r={r} color="red">
<Paint color="red" />
<Paint color="#adbce6" style="stroke" strokeWidth={strokeWidth} />
</Circle>
</Group>
</Canvas>
);
};
Tutaj mamy znacznik Paint
wewnątrz Circle
, aby wypełnić lub konturować narysowane przez nas koło. Na początku ta notacja może wydawać się dziwna, ale w rzeczywistości jest o wiele wygodniejsza do określenia relacji z różnymi API. Ta sama struktura dotyczy również Grupy. W tym przykładzie grupa posiada przeźroczystość (opacity), która wpływa na wszystko co znajduje się w środku. Ta zagnieżdżona struktura z elementami niewizualnymi przypomniała mi widżety Fluttera, takie jak Spacing, Center itp.
No dobrze, to teraz bardziej atrakcyjna część artykułu: Super filtry! Jednym z głównych powodów, dla którego chciałbyś użyć Skia, jest zdecydowanie szeroki zakres efektów graficznych i filtrów, których normalnie nie można znaleźć we frameworku React Native. Będąc silnikiem renderującym, Skia canvas posiada model shaderów, który pozwala modyfikować piksele na ekranie w dowolny sposób.
Zamiast głupiej właściwości elevation
chcesz mieć realny shadow z fajną miękkością w Androidzie? Żaden problem! Doskonałe menu z rozmytym obrazem tła? Da się zrobić. W zestawie znajduje się praktycznie każdy filtr, a także te bardziej złożone, jak Color Filter czy Displacement Map. Dodatkowo istnieją pewne generatory, takie jak Fractal Noise czy Turbulent Noise, które można połączyć z Displacement Map i sprawić, że nasz filtr będzie jeszcze ciekawszy. Jeśli żaden z nich nie jest wystarczający, zawsze możesz napisać własny shader i stworzyć coś unikalnego. Język shaderów jest bardzo podobny do GLSL, który jest używany w WebGL.
No dobra, to zróbmy szybkie demo:
import {
Canvas,
Image,
Turbulence,
DisplacementMap,
useImage,
} from "@shopify/react-native-skia";
const Filter = () => {
const image = useImage(require("./assets/girl.jpg"));
if (!image) {
return null;
}
return (
<Canvas style={{ flex: 1 }}>
<Image image={image} x={0} y={0} width={736} height={920} fit="cover">
<DisplacementMap channelX="g" channelY="a" scale={20}>
<Turbulence freqX={0.01} freqY={0.05} octaves={1} seed={2} />
</DisplacementMap>
</Image>
</Canvas>
);
};
Tutaj wybraliśmy nasz obraz i zdefiniowaliśmy mapę przemieszczeń używając Turbulence, aby wygenerować jakiś rodzaj efektu wypaczenia. Tak wyglądają efekty:
W tym przykładzie pierwszą rzeczą, którą zauważysz, jest hook useImage
. React Native Skia posiada własne elementy i są one całkowicie podobne do bazowych komponentów RN, takich jak Image. Znacznik Skia <Image>
przyjmuje obrazek w postaci typu SkImage, co można osiągnąć poprzez podanie ścieżki obrazka do hooka useImage. To dlatego, że Skia wykonuje się w wątku UI oddzielającym się od głównego wątku samego React Native. Różnicę zauważysz jeszcze wyraźniej, gdy zaczniesz działać z animacjami. Po załadowaniu naszego obrazka możemy ustawić dowolny filtr jako element dziedziczący znacznik Image. Można tu łączyć dowolne filtry, bawić się różnymi wartościami i osiągać różne rezultaty. Oczywiście nie wspominając o tym, że wartości te są również animowane, co omówimy w innym artykule :)
Na potrzeby tego artykułu chcę na koniec wspomnieć o funkcjach maskujących. Elementy maskujące nie są jakimiś jednolitymi kształtami, to raczej pojemnik, który przyjmuje dowolny kształt lub rysunek. Jeśli wcześniej używałeś react-native-mask, to jest to po prostu ten sam styl, który wygląda tak jak poniżej:
import {
Canvas,
Image,
Turbulence,
DisplacementMap,
useImage,
} from "@shopify/react-native-skia";
const Filter = () => {
const image = useImage(require("./assets/girl.jpg"));
if (!image) {
return null;
}
return (
<Canvas style={{ flex: 1 }}>
<Mask
mode="luminance"
mask={
<Group>
<Circle cx={210} cy={210} r={128} color="white" />
</Group>
}
>
<Image
image={image}
x={15}
y={0}
width={736 * 0.5}
height={920 * 0.5}
fit="cover"
/>
</Mask>
</Canvas>
);
};
Element <Mask>
przyjmuje węzeł, którym może być cokolwiek, o ile daje wartość luminancji lub alfa, na której działa element. Tutaj pokazujemy koło maskujące obraz, który wyglądałby jak awatar poniżej:
Możesz oczywiście rysować kształty, wielokąty, a nawet użyć filtrów turbulencji i sprawić, że obraz będzie wyglądał bardziej mętnie.
I to właśnie były główne funkcje React Native Skia, z których możesz korzystać. Pozostał nam jeszcze obszerny temat animacji, o którym tutaj nie wspomniałem, bo rezerwuję go na inny artykuł. Jest też kilka kompromisów, na które chciałbym zwrócić uwagę;
Mimo tych i innych problemów biblioteka jest naprawdę niezła i robi świetną robotę. Społeczność jest również bardzo aktywna i responsywna, jestem pewien, że szybko naprawią błędy w nadchodzących wersjach. Gorąco zachęcam do sprawdzenia tej biblioteki już teraz i dać upust swojej kreatywności. A do tego czasu, dobrego kodowania!
Oryginał tekstu w języku angielskim przeczytasz tutaj.