DEV Community

Cover image for Create high-performance graphics with React Native Skia
Matt Angelosanto for LogRocket

Posted on • Originally published at blog.logrocket.com

Create high-performance graphics with React Native Skia

Written by Rupesh Chaudhari✏️

Skia is a popular open-source 2D graphics library used by major platforms like Google Chrome, Chrome OS, Android, and Flutter as their default graphics engine. The library is sponsored and managed by Google, while the development is overseen by Skia’s engineering team.

What is React Native Skia?

Thanks to Shopify, William Candollin, Christian Falch, and the entire dev team behind react-native-skia, we can now use Skia in our React Native applications to draw awesome graphics and create trendy UI concepts like Neumorphism and Glassmorphism.

Note: The library is still in alpha stage and is not yet available on npm, thus, it is not completely stable yet.

You can find the complete code for this article in this GitHub repository.

Installing React Native Skia

Let’s get started with library installation in a bare React Native project. Because the library is not yet published on npm, we need to download it from their GitHub repository.

Let’s start by initializing a new react native project.

react-native init <project-name>
Enter fullscreen mode Exit fullscreen mode

After we set our project up, let’s add React Native Skia.

Run the following in your terminal:

yarn add https://github.com/Shopify/react-native-skia/releases/download/v0.1.106-alpha/shopify-react-native-skia-0.1.106.tgz
Enter fullscreen mode Exit fullscreen mode

Or, if you use npm:

npm install https://github.com/Shopify/react-native-skia/releases/download/v0.1.106-alpha/shopify-react-native-skia-0.1.106.tgz
Enter fullscreen mode Exit fullscreen mode

At the time of writing, the latest alpha release version is 0.1.106, which we’ll use in this tutorial.

Note: once newer versions are released, there will likely be some changes and new features.

Once the package is downloaded and installed, we need to set up the project in our native iOS and Android projects.

For iOS, this is pretty straightforward — we just need to install the cocoa pods dependency. Use this command to do so:

cd ios
pod install
Enter fullscreen mode Exit fullscreen mode

Now, we need to rebuild our iOS project so that we can use Skia in our React Native iOS projects.

Build the project from the terminal or directly from Xcode:

yarn ios
// OR
npm run ios
Enter fullscreen mode Exit fullscreen mode

Setting up our project in Android can be a bit complex if you’re not familiar with the environment. Because most of the graphic rendering logic for React Native Skia is written in C++, we will need something known as NDK (Android Native Development Kit) for the communication between Java code and C++ code.

“The Native Development Kit (NDK) is a set of tools that allows you to use C and C++ code with Android” - Android Documentation

To install Android NDK, you can follow these steps from the official Android docs. Now that we set our library up in both iOS and Android, we can now begin creating a beautiful user interface and graphics.

Using React Native Skia in a React Native app

React Native Skia offers two types of APIs to draw graphics:

  • A declarative API, which uses the default React Native renderer
  • An imperative API, which works with JSI (JavaScript Interface)

React Native JSI is a core change in the re-architecture of React Native. This is a layer that offers “synchronous” communication between JavaScript and native code. It will be replacing the default react native bridge. (Source)

The declarative API is similar to how we currently create UI in React Native using JSX syntax. If we want to create a rectangular shape with rounded corners, then the syntax for it will look like this:

import React from 'react';
import { Canvas, Fill, RoundedRect, vec } from '@shopify/react-native-skia';
import { StatusBar, useWindowDimensions } from 'react-native';

export const DeclarativeAPI = () => {
  const {width, height} = useWindowDimensions();
  const center = vec(width / 2, height / 2);
  return (
    <>
      <StatusBar barStyle="light-content" />
      <Canvas style={{flex: 1}}>
        <Fill color={'#222'} />
        <RoundedRect 
          height={180}
          width={300}
          x={center.x - 150}
          y={center.y - 90}
          color={'#eee'}
          rx={12}
        />
      </Canvas>
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

The output from the above code will be:

White Square

The library owners prefer users to use the declarative API, as it is easy to use and the code is readable.

Here’s what we have done in the above code. First, we needed a canvas to draw graphics on, so we wrapped our components with a Canvas component and gave it a style of flex:1 so that it takes up the entire screen space. Then, we used the Fill component to fill our Canvas with color #222.

We also declared a variable center, which is vector with two keys, X and Y. Finally, to draw our Rectangle shape with rounded corners, we used the RoundedRect component and gave it below props:

  • Height: 180, to give the rectangle a height of 180 pixels
  • Width: 300, to give the rectangle a width of 300 pixels
  • X = center.x: 150, which is the position of the rectangle from the X-axis. In React Native Skia, the X and Y positions start from the top left section
  • Y = center.y: 90, this is the position of the rectangle from the Y-axis
  • color = #eee: gives our rectangle a white color
  • rx = 12: gives our rectangle a corner radius of 12

Now, let’s see how we can create the same UI using the imperative API, which looks like this:

import React from 'react';
import {
    rect, rrect, Skia,
    SkiaView,
    useDrawCallback, vec
} from '@shopify/react-native-skia';
import { StatusBar, useWindowDimensions } from 'react-native';

const paint = Skia.Paint();
paint.setAntiAlias(true);

export const ImperativeAPI = () => {
  const {width, height} = useWindowDimensions();
  const center = vec(width / 2, height / 2);
  const rRect = rrect(rect(center.x - 150, center.y - 90, 300, 180), 12, 12);
  const onDraw = useDrawCallback(canvas => {
    const white = paint.copy();
    white.setColor(Skia.Color('#eee'));
    canvas.drawRRect(rRect, white);
  }, []);
  return (
    <>
      <StatusBar barStyle="light-content" />
      <SkiaView style={{flex: 1, backgroundColor: '#222'}} onDraw={onDraw} />
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

The output for the above is identical to the one created by the declarative API.

White Square

Here, we created a SkiaView, which is essentially our Canvas, and we are using its onDraw callback to define what to draw on the view. Then, there is the onDraw method, which uses the useDrawCallback hook and returns a memoized callback. In the function, we are initializing Paint, then using that paint as the color of our rounded rectangle that we drew on the canvas.

Adding filters to images in React Native Skia

Now, let’s see how you can display an image using Skia’s Image component, as well as how to add cool filters to the image in real time.

Let’s write code to display the image first.

import {Canvas, Image, useImage} from '@shopify/react-native-skia';
import React from 'react';
import {Dimensions, SafeAreaView, StyleSheet, Text} from 'react-native';

const {height, width} = Dimensions.get('window');

export const ImageFilters = () => {
  const image = useImage(require('../assets/image.jpg'));
  if (image === null) return null;
  return (
    <SafeAreaView>
      <Text style={styles.title}>Image Filters</Text>
      <Canvas style={styles.canvas}>
        <Image
          x={0}
          y={0}
          width={width - 32}
          height={height - 300}
          image={image}
          fit="cover"
        />
      </Canvas>
    </SafeAreaView>
  );
};
const styles = StyleSheet.create({
  title: {
    paddingVertical: 16,
    paddingHorizontal: 16,
    fontWeight: '700',
    fontSize: 32,
    letterSpacing: 1.4,
  },
  canvas: {
    height: height - 300,
    marginHorizontal: 16,
    width: width - 32,
  },
});
Enter fullscreen mode Exit fullscreen mode

In the above code, we initialize the image variable using the useImage hook from react-native-skia.

We can use this hook to initialize local image assets with the help of require helper function, or we can also use remote images from the network, like this:

const image = useImage('https://picsum.photos/1920/1080');
Enter fullscreen mode Exit fullscreen mode

Then, we wrap the Image component from Skia in a Canvas and add styles to the components. The output of the above code will be:

Image Filters

Now that our image is visible, let’s add some filters to it using the color matrix, which is a matrix (2D-Array) with values for RGBA colors. The syntax is something like this:

| R' |   | a00 a01 a02 a03 a04 |   | R |
| G' |   | a10 a11 a22 a33 a44 |   | G |
| B' | = | a20 a21 a22 a33 a44 | * | B |
| A' |   | a30 a31 a22 a33 a44 |   | A |
Enter fullscreen mode Exit fullscreen mode

We will use the ColorMatrix filter component from React Native Skia to achieve this.

First, let’s create matrices for all the filters we will be using:

const filters = {
    Juno: [
        1, 0, 0, 0, 0,
        -0.4, 1.3, -0.4, 0.2, -0.1,
        0, 0, 1, 0, 0,
        0, 0, 0, 1, 0,
    ],
    Sepia: [
        0.393, 0.769, 0.189, 0, 0,
        0.349, 0.686, 0.168, 0, 0,
        0.272, 0.534, 0.131, 0, 0,
        0,     0,     0,     1, 0,
    ],
    Greyscale: [
        0.2126, 0.7152, 0.0722, 0, 0,
        0.2126, 0.7152, 0.0722, 0, 0,
        0.2126, 0.7152, 0.0722, 0, 0,
        0,      0,      0,      1, 0,
    ],
    Gingham: [
        2, 0, 0, 0, 0,
        1, 1, 0, 0, 0,
        0.5, 0, 1, 0, 0,
        0, 0, 0, 1, 0,
    ],
    Mayfair: [
        1, 1, 0.5, 0, 0,
        0, 0.5, 1, 0, 0,
        0.5, 0.5, 1, 0, 0,
        0, 0, 0, 1, 0,
    ],
    Valencia: [
        1, 0, 0, 0, 0,
        -0.2, 1, 0, 0, 0,
        -0.8, 1.6, 1, 0, 0,
        0, 0, 0, 1, 0,
    ],
    'No Filter': [
        1, 0, 0, 0, 0,
        0, 1, 0, 0, 0,
        0, 0, 1, 0, 0,
        0, 0, 0, 1, 0,
    ]
};
Enter fullscreen mode Exit fullscreen mode

You can also create your own custom color matrix, there are many playgrounds available. Note that we created an object with seven keys, each pointing to a ColorMatrix value.

After adding some code to handle our UI, the final code for this will be:

import {
  Canvas,
  ColorMatrix,
  Image,
  Paint,
  useImage,
} from '@shopify/react-native-skia';
import React, {useCallback, useState} from 'react';
import {
  Dimensions,
  FlatList,
  SafeAreaView,
  StyleSheet,
  Text,
} from 'react-native';

const {height, width} = Dimensions.get('window');
const filters = {
    Juno : [
        1, 0, 0, 0, 0,
        -0.4, 1.3, -0.4, 0.2, -0.1,
        0, 0, 1, 0, 0,
        0, 0, 0, 1, 0,
    ],
    Sepia: [
        0.393, 0.769, 0.189, 0, 0,
        0.349, 0.686, 0.168, 0, 0,
        0.272, 0.534, 0.131, 0, 0,
        0,     0,     0,     1, 0,
    ],
    Greyscale: [
        0.2126, 0.7152, 0.0722, 0, 0,
        0.2126, 0.7152, 0.0722, 0, 0,
        0.2126, 0.7152, 0.0722, 0, 0,
        0,      0,      0,      1, 0,
    ],
    Gingham: [
        2, 0, 0, 0, 0,
        1, 1, 0, 0, 0,
        0.5, 0, 1, 0, 0,
        0, 0, 0, 1, 0,
    ],
    Mayfair: [
        1, 1, 0.5, 0, 0,
        0, 0.5, 1, 0, 0,
        0.5, 0.5, 1, 0, 0,
        0, 0, 0, 1, 0,
    ],
    Valencia: [
        1, 0, 0, 0, 0,
        -0.2, 1, 0, 0, 0,
        -0.8, 1.6, 1, 0, 0,
        0, 0, 0, 1, 0,
    ],
    ['No Filter']: [
        1, 0, 0, 0, 0,
        0, 1, 0, 0, 0,
        0, 0, 1, 0, 0,
        0, 0, 0, 1, 0,
    ]
};

export const ImageFilters = () => {
  const [selectedFilter, setSelectedFilter] = useState('Juno');
  const handlePress = useCallback(
    item => () => {
      setSelectedFilter(item);
    },
    [],
  );
  const image = useImage(require('../assets/image.jpg'));
  if (image === null) return null;
  return (
    <SafeAreaView>
      <Text style={styles.title}>Image Filters</Text>
      <Canvas style={styles.canvas}>
        <Paint>
            <ColorMatrix 
              matrix={filters[selectedFilter]}
            />
        </Paint>
        <Image
          x={0}
          y={0}
          width={width - 32}
          height={height - 300}
          image={image}
          fit="cover"
        />
      </Canvas>
      <FlatList
        numColumns={3}
        data={Object.keys(filters)}
        keyExtractor={(_, index) => index}
        renderItem={({item}) => (
          <Text
            style={[
              styles.item,
              selectedFilter === item && styles.selectedItem,
            ]}
            onPress={handlePress(item)}>
            {item}
          </Text>
        )}
        style={styles.list}
      />
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  title: {
    paddingVertical: 16,
    paddingHorizontal: 16,
    fontWeight: '700',
    fontSize: 32,
    letterSpacing: 1.4,
    color: '#000',
  },
  canvas: {
    height: height - 300,
    marginHorizontal: 16,
    width: width - 32,
    borderRadius: 12,
  },
  list: {
    margin: 16,
  },
  item: {
      width: '33%',
      textAlign: 'center',
      marginBottom: 12,
      fontWeight: '600',
      fontSize: 18,
  },
  selectedItem: {
      color: '#ea4c89',
      borderWidth: 1,
      borderColor: '#ea4c89',
      borderRadius: 12,
 },
});
Enter fullscreen mode Exit fullscreen mode

The output from the above code in iOS and Android are:

Choosing Different Filters

Choosing More Filters

That’s it! We’ve now added filters to our images in React Native.

Creating neumorphic UI with React Native Skia

With Skia, we can create neumorphic UI in React Native. React Native Skia provides a DropShadow component for creating shadows, similar to how we would in web apps to create a neumorphic design pattern.

Below is the code to create a neumorphic list:

import {
  Canvas,
  DropShadow,
  Group,
  Paint,
  RoundedRect,
  Text,
  useFont,
} from '@shopify/react-native-skia';
import React from 'react';
import {Dimensions, ScrollView} from 'react-native';

const data = [
  {
    id: 1,
    name: 'John Doe',
    body: 'Sunt aliqua eu officia eiusmod non',
  },
  {
    id: 2,
    name: 'Jane Doe',
    body: 'Sunt aliqua eu officia eiusmod non',
  },
  {
    id: 3,
    name: 'Erin Yeager',
    body: 'Adipisicing sint ullamco irure nulla',
  },
  {
    id: 4,
    name: 'Mikasa Ackerman',
    body: 'Adipisicing sint ullamco irure nulla',
  },
  {
    id: 5,
    name: 'John Doe',
    body: 'Sunt aliqua eu officia eiusmod non',
  },
  {
    id: 6,
    name: 'Jane Doe',
    body: 'Sunt aliqua eu officia eiusmod non',
  },
  {
    id: 7,
    name: 'John Doe',
    body: 'Sunt aliqua eu officia eiusmod non',
  },
  {
    id: 8,
    name: 'Jane Doe',
    body: 'Sunt aliqua eu officia eiusmod non',
  },
];

const {width} = Dimensions.get('window');

const NeumorphicCard = ({data, index, font, bodyFont}) => {
  return (
    <Canvas style={{height: 140, width}} key={index}>
      <Group>
        <Paint>
          <DropShadow dx={6} dy={6} blur={4} color="rgba(0, 0, 0, 0.4)" />
          <DropShadow dx={-6} dy={-6} blur={4} color="rgba(255, 255, 255, 1)" />
        </Paint>
        <RoundedRect
          x={16}
          y={20}
          width={width - 32}
          height={100}
          rx={12}
          color="#EFEEEE">
          <Paint
            color="rgba(255, 255, 255, 0.6)"
            style="stroke"
            strokeWidth={1}
          />
        </RoundedRect>
      </Group>
      <Text x={32} y={56} text={data?.name} font={font} />
      <Text x={32} y={92} text={data?.body} font={bodyFont} />
    </Canvas>
  );
};

export const Neumorphism = () => {
  const font = useFont(require('../fonts/Poppins-Regular.ttf'), 20);
  const bodyFont = useFont(require('../fonts/Poppins-Regular.ttf'), 14);
  if (font === null || bodyFont === null) {
    return null;
  }
  return (
    <ScrollView
      showsVerticalScrollIndicator={false}
      style={{flex: 1, backgroundColor: '#EFEEEE'}}
      contentContainerStyle={{
        paddingTop: Platform.OS === 'ios' ? 40 : 0,
        paddingBottom: 40,
      }}>
      {data.map(item => (
        <NeumorphicCard
          key={item.id}
          data={item}
          font={font}
          bodyFont={bodyFont}
        />
      ))}
    </ScrollView>
  );
};
Enter fullscreen mode Exit fullscreen mode

Above, we are mapping through an array of random data, and, for each item, we are rendering a NeumorphicCard component. We are also using the useFont hook to load custom fonts.

In the card component, we have a RoundedRect with a border of 1 unit, which we have created using Paint with the style of stroke. Then, we added two DropShadow components to create a shadow on the top-left and bottom-right side of the rectangle.

Here is the output of the above code in iOS and Android:

Drag Scroll

Drag Scroll on Android

Incorporating glassmorphism with React Native Skia.

Finally, let’s create a glassmorphism-inspired design in our React Native app. To create this glassmorphic design, we will need the BackDropBlur component. We will create a RoundedRect, then give the background a blur using the BackDropBlur component. We will also see how we can animate graphics.

Write the below code to create an animated glassmorphic card:

import {
  BackdropBlur, Canvas, Fill, Group, Image, mix,
  Paint, rect, Rect, RoundedRect, rrect, Text,
  useDerivedValue, useFont, useImage, useLoop, vec,
} from '@shopify/react-native-skia';
import React from 'react';
import {useWindowDimensions} from 'react-native';

export const Glassmorphism = () => {
  const {width, height} = useWindowDimensions();
  const center = vec(width / 2 - 50, height / 2 - 100);
  const blurClipPath = rrect(rect(24, center.y, width - 48, 200), 12, 12);
  const image = useImage('https://picsum.photos/1920/1080');
  const blurProgress = useLoop({duration: 2000});
  const blur = useDerivedValue(
    progress => mix(progress, 0, 10),
    [blurProgress],
  );
  const font = useFont(require('../fonts/Poppins-Regular.ttf'), 40);
  if (font === null) {
    return null;
  }
  return (
    <Canvas style={{flex: 1}}>
      <Group>
        <Rect x={0} y={0} width={width} height={height} />
        <Image
          x={0}
          y={0}
          width={width}
          height={height}
          image={image}
          fit={'cover'}
        />
      </Group>
      <Group>
        <RoundedRect
          x={24}
          y={center.y}
          width={width - 48}
          height={200}
          color="#0000"
          rx={12}>
          <Paint
            color="rgba(255, 255, 255, 0.8)"
            style="stroke"
            strokeWidth={1}
          />
        </RoundedRect>
        <BackdropBlur blur={blur} clip={blurClipPath}>
          <Fill color={'rgba(122, 122, 122, 0.2)'} />
        </BackdropBlur>
        <Text
          x={center.x - 50}
          y={center.y + 110}
          text="Hello Skia"
          font={font}
          color="#fff"
          style="stroke"
        />
      </Group>
    </Canvas>
  );
};
Enter fullscreen mode Exit fullscreen mode

The output will look like this:

Hello Skia in Square

Conclusion

Now you’ve seen just how powerful React Native Skia is, and I think it will change how we create UI in React Native. Because the library uses the full potential of the new React Native architecture (JSI) and the new renderer, it will surely increase our React Native application’s performance. Thanks for reading!


LogRocket: Instantly recreate issues in your React Native apps.

LogRocket React Native monitoring solution

LogRocket is a React Native monitoring solution that helps you reproduce issues instantly, prioritize bugs, and understand performance in your React Native apps.

LogRocket also helps you increase conversion rates and product usage by showing you exactly how users are interacting with your app. LogRocket's product analytics features surface the reasons why users don't complete a particular flow or don't adopt a new feature.

Start proactively monitoring your React Native apps — try LogRocket for free.

Top comments (0)