Pull to refresh

Skia — высокопроизводительная 2D графика для React Native

Reading time5 min
Views8.3K

Пользователям Flutter не понаслышке знаком такой проект как Skia. Он является движком для рендеринга всего что мы видим на экране Flutter. С помощью него можно рисовать сложные элементы интерфейса и любые 2D сцены с поддержкой плавной анимации и различных эффектов. Так почему бы не взять это на вооружение, подумали ребята из Shopify и выпустили React Native Skia - библиотеку позволяющую использовать Skia в экосистеме React Native.

Для того чтобы посмотреть на что способна Skia предлагаю использовать example из репозитория библиотеки.

Примеры интерфейсов созданных с помощью React Native Skia
Примеры интерфейсов созданных с помощью React Native Skia

Графика

Начнем с базовой графики.

import React from 'react';
import { Group, Rect, RoundedRect, DiffRect, Canvas, rrect } from '@shopify/react-native-skia';
import { StatusBar, useWindowDimensions } from 'react-native';

const PADDING = 16;

export const Declarative = () => {
  const { width } = useWindowDimensions();
  const SIZE = width / 4;
  const style = useMemo(() => ({ width, height: SIZE + 32 }), [SIZE, width]);
  const outer = useMemo(
    () => rrect(rect(2 * SIZE + 3 * 16, PADDING, SIZE, SIZE), 25, 25),
    [SIZE]
  );
  const inner = useMemo(
    () =>
      rrect(
        rect(2 * SIZE + 4 * PADDING, 2 * PADDING, SIZE - 32, SIZE - 32),
        0,
        0
      ),
    [SIZE]
  );
  return (
    <>
      <Canvas style={style}>
        <Group color="#61DAFB">
          <Rect rect={{ x: PADDING, y: PADDING, width: 100, height: 100 }} />
          <RoundedRect
            x={SIZE + 2 * PADDING}
            y={PADDING}
            width={SIZE}
            height={SIZE}
            r={25}
          />
          <DiffRect outer={outer} inner={inner} />
        </Group>
      </Canvas>
    </>
  );
};


Canvas - это корневой элемент для рисования с помощью Skia. К Canvas применяются стили как и к компоненту View с помощью свойства style. Помимо этого с помощью Canvas обрабатываются touch события передав функцию в свойство onTouch.

const MyComponent = () => {
  const cx = useValue(100);
  const cy = useValue(100);
 
  const touchHandler = useTouchHandler({
    onActive: ({ x, y }) => {
      cx.current = x;
      cy.current = y;
    },
  });
 
  return (
    <Canvas onTouch={touchHandler}>
      <Circle cx={cx} cy={cy} r={10} color="red" />
    </Canvas>
  );
};

Компоненты React, RoundedRect, Circle, DiffRect, Line, Point и прочие фигуры используются для рисования фигур. Каждый компонент имеет свои специфичные свойства и свойства общие для любых компонентов которые мы рисуем с помощью Skia такие, как color, blendMode, style и т.д. Очень полезным может быть использование компонента Group который позволяет применять общие свойства всем дочерним компонентам.

export const PaintDemo = () => {
  const r = 128;
  return (
    <Canvas style={{ flex: 1 }}>
      <Circle cx={r} cy={r} r={r} color="#51AFED" />
      <Group color="lightblue" style="stroke" strokeWidth={10}>
        <Circle cx={r} cy={r} r={r / 2} />
        <Circle cx={r} cy={r} r={r / 3} color="white" />
      </Group>
    </Canvas>
  );
};

Продолжая тему рисования - ко всем фигурам мы можем применять различные маски, эффекты, фильтры и трансформации. Например BlurMask

const MaskFilterDemo = () => {
  return (
    <Canvas style={{ flex: 1}}>
      <Circle c={vec(128)} r={128} color="lightblue">
        <BlurMask blur={20} style="normal" />
      </Circle>
    </Canvas>
  );
};
const SimpleTransform = () => {
  return (
    <Canvas style={{ flex: 1 }}>
      <Fill color="#e8f4f8" />
      <Group
        color="lightblue"
        origin={{ x: 128, y: 128 }}
        transform={[{ skewX: Math.PI / 6 }]}
      >
        <RoundedRect x={64} y={64} width={128} height={128} r={10} />
      </Group>
    </Canvas>
  );
};

Помимо рисования Skia поддерживает работы с изображениями и SVG. Изображения используются как отдельный компонент или могут быть вписаны в другие фигуры.

const Clip = () => {
  const image = useImage(require("./assets/oslo.jpg"));
  const star = Skia.Path.MakeFromSVGString(
    "M 128 0 L 168 80 L 256 93 L 192 155 L 207 244 L 128 202 L 49 244 L 64 155 L 0 93 L 88 80 L 128 0 Z"
  )!;
  if (!image) {
    return null;
  }
  return (
    <Canvas style={{ flex: 1 }}>
      <Group clip={star}>
        <Image
          image={image}
          x={0}
          y={0}
          width={256}
          height={256}
          fit="cover"
        />
      </Group>
    </Canvas>
  );
};

Отдельно стоит упомянуть поддержку шейдорв. В Skia реализован свой язык похожий на GLSL. Пример использования простейшего шейдера:

import {Skia, Canvas, Shader, Fill} from "@shopify/react-native-skia";
 
const source = Skia.RuntimeEffect.Make(`
vec4 main(vec2 pos) {
  // normalized x,y values go from 0 to 1, the canvas is 256x256
  vec2 normalized = pos/vec2(256);
  return vec4(normalized.x, normalized.y, 0.5, 1);
}`)!;
 
const SimpleShader = () => {
  return (
    <Canvas style={{ width: 256, height: 256 }}>
      <Fill>
        <Shader source={source} />
      </Fill>
    </Canvas>
  );
};

Анимация

Поддержка анимаций строиться на концепции Skia Values. С помощью Value мы храним состояние, которое может быть ассоциировано с объектом на Canvas и при изменении этого состояние объект будет перерисован. В качестве значения могут быть использованы строки, числа, объекты и массивы.

const MyComponent = () => {
  const position = useValue(0);
  const updateValue = useCallback(
    () => (position.current = position.current + 10),
    [position]
  );
 
  return (
    <>
      <Canvas style={{ flex: 1 }}>
        <Rect x={position} y={100} width={10} height={10} color={"red"} />
      </Canvas>
      <Button title="Move it" onPress={updateValue} />
    </>
  );
};

С помощью хука useComputedValue можно рассчитать новое значения основываясь на другие values, а с помощью useValueEffect реагировать на изменения values. Для работы со сложными объектами или массивами нужно использовать функцию Selector которая принимаем на вход value и возвращает значение которое используется в конкретном свойстве объекта на Canvas.

const Heights = new Array(10).fill(0).map((_, i) => i * 0.1);
 
export const Demo = () => {
  const loop = useLoop();
  const heights = useComputedValue(
    () => Heights.map((_, i) => loop.current * i * 10),
    [loop]
  );
 
  return (
    <Canvas style={{ flex: 1, marginTop: 50 }}>
      {Heights.map((_, i) => (
        <Rect
          key={i}
          x={i * 20}
          y={0}
          width={16}
          height={Selector(heights, (v) => v[i])}
          color="red"
        />
      ))}
    </Canvas>
  );
};

Для облегчения работы с values фреймворк поставляется с набором хуков таких как useTiming, useLoop, useSpring и функций interpolate, interpolatePaths, interpolateColors, runDecay для построение анимаций. Пример использования хуков

export const AnimationExample = () => {
  const [toggled, setToggled] = useState(false);
  const position = useSpring(toggled ? 100 : 0);
  return (
    <>
      <Canvas style={{ flex: 1 }}>
        <Rect x={position} y={100} width={10} height={10} color={"red"} />
      </Canvas>
      <Button title="Toggle" onPress={() => setToggled((p) => !p)} />
    </>
  );
};

Skia API

Кроме работы в декларативном стиле библиотека так же предлагает доступ к API Skia напрямую используя новые возможности React Native по синхронной коммуникации с нативным кодом (JSI). Это API практически на 100% совместимо с Flutter API. Пример использования.

import {Skia, SkiaView, useDrawCallback} from "@shopify/react-native-skia";
 
export const HelloWorld = () => {
  const r = 128;
  const onDraw = useDrawCallback((canvas) => {
    const paint = Skia.Paint();
    paint.setAntiAlias(true);
    cyan.setColor(Skia.Color("cyan"));
    canvas.drawCircle(r, r, r, paint);
  });
  return (
    <SkiaView style={{ flex: 1 }} onDraw={onDraw} />
  );
};

Выводы

Как видно из примеров в начале статьи фреймворк позволяет использовать много возможностей Skia для проектирования сложных интерфейсов. Поддержка JSI гарантирует минимальный оверхед при работе с нативным движком, это позволит добиться высокой производительности. Из плюсов отмечу поддержку от Shopify, проект скорее всего не будет заброшен и будет активно развиваться.

Из минусов неполная поддержка API Skia и статус Alpha версии библиотеки, API меняется от версии к версии, а частые релизы приносят баги и ломают обратную совместимость.

Подробнее о возможностях React Native Skia читайте в документации https://shopify.github.io/react-native-skia/ а примеры использования на канале одного из разработчиков William Candillon

Также подписывайтесь на мой Телеграм-канал React Native World — там я размещаю последние новости, обзоры и статьи из мира React Native.

Tags:
Hubs:
Total votes 4: ↑4 and ↓0+4
Comments8

Articles