7.10.20227 min

Burak Erdem

Poznaj React Native Skia

Dowiedz się, czym jest oraz jak działa Skia na React Native w JavaScript.

Poznaj React Native Skia

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.


Kształty i ścieżki

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. 


Efekty i filtry

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 :)


Maski

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.


Wnioski

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ę;

  • Biblioteka jest stosunkowo nowa i znajduje się w fazie pomiędzy alfa a beta. Tak więc spodziewaj się błędów i problemów z wydajnością.
  • Jeśli chodzi o wydajność, unikaj używania go jako elementu FlatList lub do czegoś generowanego powtarzalnie.
  • Czasami własne shadery nie działają poprawnie
  • Kiedy element Group ma wiele dzieci, problemy z wydajnością mogą się również pojawić. Staraj się utrzymywać liczbę na niskim poziomie.


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.

<p>Loading...</p>