How to improve performance of bitmap operations with react-native-reanimated and react-native-skia

52 views Asked by At

I'm creating a pixel art editor with react-native-reanimated and react-native-skia, and I was wondering how I can improve performance?

I have some code which manipulates a uint8array and converts it to an image which is passed to Image component from react native skia.

Currently, the code just isn't as fast as I'd like it to be and I was wondering whether or not this is an issue on my end or just due to the limitations of React Native and the libraries I'm using:

import {useEffect, useMemo, useRef, useState} from "react";
import {
  AlphaType,
  Canvas, ColorType,
  createPicture,
  Image,
  notifyChange,
  PointMode,
  Skia,
  SkImage,
  useCanvasRef
} from "@shopify/react-native-skia";
import {StyleSheet, View, useWindowDimensions, GestureResponderEvent} from "react-native";
import Animated, {runOnJS, useAnimatedReaction, useDerivedValue, useSharedValue} from "react-native-reanimated";
import {Button} from "react-native-paper";
import {Gesture, GestureDetector, GestureHandlerRootView} from "react-native-gesture-handler";

const size = 100

function newArr(): Uint8Array {
  const arr = new Uint8Array(size * size * 4)
  arr.fill(255)

  return arr
}

export interface Coordinate {
  x: number
  y: number
}

function drawLineY(pixels: Uint8Array, from: Coordinate, to: Coordinate) {
  'worklet'
  let x = from.x
  let y = from.y

  const differenceX = to.x - x
  let differenceY = to.y - y

  let yi = 1
  const xi = 1

  if (differenceY < 0) {
    differenceY = -differenceY
    yi = -1
  }

  let p = 2 * differenceY - differenceX

  while (x <= to.x) {
    const index = ((y) * size + (x)) * 4;
    pixels[index] = 0
    pixels[index + 1] = 0
    pixels[index + 2] = 0
    x++

    if (p < 0) {
      p += 2 * differenceY
      if (differenceY > differenceX) {
        x += xi
      }
    } else {
      p = p + 2 * differenceY - 2 * differenceX
      y += yi
    }
  }
}

function drawLineX(pixels: Uint8Array, from: Coordinate, to: Coordinate) {
  'worklet'
  let x = from.x
  let y = from.y

  let differenceX = to.x - x
  const differenceY = to.y - y

  let xi = 1

  if (differenceX <= 0) {
    differenceX = -differenceX
    xi = -1
  }

  let p = 2 * differenceX - differenceY

  while (y <= to.y) {
    const index = ((y) * size + (x)) * 4;
    pixels[index] = 0
    pixels[index + 1] = 0
    pixels[index + 2] = 0
    y++

    if (p < 0) {
      p += 2 * differenceX
    } else {
      p = p + 2 * differenceX - 2 * differenceY
      x += xi
    }
  }
}

function drawLine(pixels: Uint8Array, from: Coordinate, to: Coordinate) {
  'worklet'
  const x = from.x
  const y = from.y

  const differenceX = to.x - x
  const differenceY = to.y - y

  if (differenceY <= differenceX) {
    if (Math.abs(differenceY) > differenceX) {
      drawLineX(pixels, to, from)
    } else {
      drawLineY(pixels, from, to)
    }
  } else {
    if (Math.abs(differenceX) > differenceY) {
      drawLineY(pixels, to, from)
    } else {
      drawLineX(pixels, from, to)
    }
  }
}

export default function PixelCanvas() {
  const pixels = useSharedValue(newArr());
  const coordinateTapped = useSharedValue<{ x: number, y: number } | null>(null)
  const prevCoordinateTapped = useSharedValue<{ x: number, y: number } | null>(null)

  const SIZE = 1000;
  const img = useDerivedValue(() => {
    // If we are not on the UI thread, don't do anything.
    if (!_WORKLET) {
      return;
    }

    if (coordinateTapped.value) {
      const index = ((coordinateTapped.value!.y) * size + (coordinateTapped.value!.x)) * 4;

      pixels.value[index] = 0
      pixels.value[index + 1] = 0
      pixels.value[index + 2] = 0

      if (prevCoordinateTapped.value) {
        drawLine(pixels.value, prevCoordinateTapped.value!, coordinateTapped.value!)
      }

      prevCoordinateTapped.value = coordinateTapped.value
    }

    const data = Skia.Data.fromBytes(pixels.value);
    return Skia.Image.MakeImage(
      {
        width: size,
        height: size,
        alphaType: AlphaType.Opaque,
        colorType: ColorType.RGBA_8888,
      },
      data,
      size * 4
    )!;
  }, [coordinateTapped]);
  
  function touchHandler(e: GestureResponderEvent) {
    const x = Math.floor(e.nativeEvent.locationX / (300 / size))
    const y = Math.floor(e.nativeEvent.locationY / (300 / size))

    coordinateTapped.value = { x, y }
  }

  return (
    <GestureHandlerRootView onTouchMove={touchHandler} onTouchStart={touchHandler} onTouchEnd={touchHandler}>
      <Canvas
        style={{ width: SIZE, height: SIZE }}
      >
        <Image image={img} x={0} y={0} width={300} height={300} fit="cover" />
      </Canvas>
    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  canvas: {
    flexGrow: 1,
    backgroundColor: 'blue',
  },
})

There is a noticeable delay when the user draws, which is quite frustrating as in native (using android bitmap) it's very fast.

I'm trying to think of what I can possibly do to improve speeds, but I just don't know what.

Perhaps it's because SkBitmap isn't directly exposed by the library?

0

There are 0 answers