Shopify / react-native-skia

High-performance React Native Graphics using Skia
https://shopify.github.io/react-native-skia
MIT License
6.91k stars 446 forks source link

SkBitmap #2199

Closed tomdoeslinux closed 8 months ago

tomdoeslinux commented 8 months ago

Description

I don't know how I'm meant to manipulate pixels in this library?

I didn't find a SkBitmap at all, and manipulating image pixels seems to be slow.

wcandillon commented 8 months ago

Could you provide more information about your use-case? You can manipulate the pixels directly in memory like in this example: https://github.com/Shopify/react-native-skia/blob/main/example/src/Examples/API/Data.tsx#L11

tomdoeslinux commented 8 months ago

Could you provide more information about your use-case? You can manipulate the pixels directly in memory like in this example: https://github.com/Shopify/react-native-skia/blob/main/example/src/Examples/API/Data.tsx#L11

Thanks for replying :)

I love what you have done with this library, but there seems to be some performance issues, which (imo) largely stem from the fact that everything is tightly linked to React components/state, if that makes sense?

I already have created the pixel view like the exmaple but the issue is that the <Image> is a component and I have to store its 'image' prop in state. Whenever I update the state, it seems to bottlenecking everything, leading to mundane performance. This could be because of the React Native - Android bridge, but I'm not sure.

More specifically, I did a test and found that setImage (updating the img prop) is taking a 'long' time (compared to the rest of the oeprations) which I don't get when using native Android bitmaps.

Is there a faster way to update the img prop? Am I getting something wrong?

If you don't mind reviewing some code, here is what I have done:

export class Bitmap {
  private static readonly RGBA_COMPONENTS = 4
  private readonly width: number
  private readonly height: number
  private readonly pixels: Uint8Array

  constructor(width: number, height: number) {
    this.width = width
    this.height = height
    this.pixels = new Uint8Array(width * height * Bitmap.RGBA_COMPONENTS)

    for (let i = 0; i < this.pixels.length; i += Bitmap.RGBA_COMPONENTS) {
      this.pixels[i] = 0
      this.pixels[i + 1] = 0
      this.pixels[i + 2] = 0
      this.pixels[i + 3] = 255
    }
  }

  clear() {
    this.pixels.fill(0)
  }

  setPixel(x: number, y: number): void {
    const index = (y * this.width + x) * Bitmap.RGBA_COMPONENTS;

    this.pixels[index] = 255
    this.pixels[index + 1] = 255
    this.pixels[index + 2] = 0
  }

  drawLine(from: Coordinate, to: Coordinate) {
    drawLine(this, from, to)
  }

  getWidth(): number {
    return this.width
  }

  getHeight(): number {
    return this.height
  }

  getImage(): SkImage {
    const data = Skia.Data.fromBytes(this.pixels)

    const img = Skia.Image.MakeImage(
      {
        width: this.width,
        height: this.height,
        alphaType: AlphaType.Opaque,
        colorType: ColorType.RGBA_8888,
      },
      data,
      this.width * Bitmap.RGBA_COMPONENTS
    )

    return img!
  }
}
export default function PixelCanvas() {
  const bitmapRef = useRef(new Bitmap(500, 500))
  const [image, setImage] = useState<SkImage | null>(bitmapRef.current.getImage())
  const prevX = useRef<number | null>(null);
  const prevY = useRef<number | null>(null);
  const canvasRef = useCanvasRef()

  const imageWidth = useMemo(() => bitmapRef.current.getWidth(), [])
  const imageHeight = useMemo(() => bitmapRef.current.getHeight(), [])

  const scaleX =  useMemo(() => imageWidth / bitmapRef.current.getWidth(), [])
  const scaleY =  useMemo(() => imageHeight / bitmapRef.current.getHeight(), [])

  createPicture((c) => {
    const paint = Skia.Paint();
    paint.setColor(Skia.Color("cyan"))
    c.drawPoints(PointMode.Points, [{ x: 0, y: 0 }], paint)
  })

  function touchHandler(e: GestureResponderEvent) {
    const bitmap = bitmapRef.current
    const x = Math.floor(e.nativeEvent.locationX / scaleX)
    const y = Math.floor(e.nativeEvent.locationY / scaleY)

    bitmap.setPixel(x, y)
    setImage(bitmap.getImage())

    prevX.current = x
    prevY.current = y

    bitmapRef.current.clear()
    bitmapRef.current.drawLine({ x: 0, y: 0 }, { x: prevX.current ?? 0, y: prevY.current ?? 0 })
    setImage(bitmapRef.current.getImage())
  }

  return (
    <Canvas
      style={styles.canvas}
      onTouchMove={touchHandler}
      onTouchStart={touchHandler}
      onTouchEnd={touchHandler}
    >
      <Image
        image={image}
        fit="contain"
        x={0}
        y={0}
        width={imageWidth}
        height={imageHeight}
      />
    </Canvas>
  )
}

const styles = StyleSheet.create({
  canvas: {
    flexGrow: 1,
    backgroundColor: 'blue',
  },
})
wcandillon commented 8 months ago

Thanks for sending a clear and concise example. This is very useful.

To draw gestures on the canvas, this might not be the right approach, unless you want to do something like pixel art (if you scale the bitmaps you will get some nice looking pixel grid). I will assume that indeed this is what you are trying to achieve.

The way to make this example fast and run smoothly is to have the bitmap live on the UI thread: https://shopify.github.io/react-native-skia/docs/animations/animations This means:

tomdoeslinux commented 8 months ago
  • Reanimated

Thanks for your quick response :+1:

I'm really new to this so I'm unsure what you mean by 'Reanimated' as it seems to be a separate library. And I'm not going after animations, just regular pixel drawing.

When it comes to whether or not I'm doing things the right way, I honestly don't know. But with Android I directly just used a Bitmap to manipulate everything and the performance was pretty good.

I'm not sure whether or not I can use a regular canvas and just change its resolution (I'm not well versed in 2D graphics). As you said, not sure the pixel grid would look nice.

Thanks for your tip. I'll try to refactor the code to use a non-class-based system.

In your example you used a Uint8Array, so I assume it's supported. But you are talking about Reanimated, which I am unsure about.

Thanks for taking in the time to build my code.

wcandillon commented 8 months ago

if you want to animate anything in React Native, you will need to use Reanimated as mentioned in the docs. It also works well with react-native-gesture-handler, most of our examples are built with these two libraries. Here's one for instance: https://github.com/Shopify/react-native-skia/blob/main/example/src/Examples/Hue/Hue.tsx

tomdoeslinux commented 8 months ago

if you want to animate anything in React Native, you will need to use Reanimated as mentioned in the docs. It also works well with react-native-gesture-handler, most of our examples are built with these two libraries. Here's one for instance: https://github.com/Shopify/react-native-skia/blob/main/example/src/Examples/Hue/Hue.tsx

Ah, I get what you mean. By animating I assume you mean something changing very often on screen, for example pixels being drawn is a sort of animation? (Sorry I am not new to this terminology.)

If that's the case, I'll reimplement it using the ReAnimated.

tomdoeslinux commented 8 months ago

Unfortunately, it just doesn't seem like Uint8Array works in reanimated.

Here's the code I tried, which is just giving me an 'undefined':

function createPixels(): Uint8Array {
  const pixels = new Uint8Array(500 * 500 * RGBA_COMPONENTS)

  for (let i = 0; i < pixels.length; i += RGBA_COMPONENTS) {
    pixels[i] = 0
    pixels[i + 1] = 0
    pixels[i + 2] = 0
    pixels[i + 3] = 255
  }

  return pixels
}

export default function PixelCanvas() {
  const pixels = useSharedValue(createPixels())
  /// ....
}

I don't want to bother you too much, so if it isn't currently possible to do what I want with this library then I am fine with going native for drawing these bitmaps. If you can possibly fix this by adding SkBitmap that would also be great.

Thanks!

tomdoeslinux commented 8 months ago

Any updates?

wcandillon commented 8 months ago

I would take a bit of time for me to get into it but on your side you can read a bit on reanimated on how it works, it will be useful either=way. The code you pasted would need look like this:

// Add a worklet directive
function createPixels(): Uint8Array {
 "worklet";
  const pixels = new Uint8Array(500 * 500 * RGBA_COMPONENTS)

  for (let i = 0; i < pixels.length; i += RGBA_COMPONENTS) {
    pixels[i] = 0
    pixels[i + 1] = 0
    pixels[i + 2] = 0
    pixels[i + 3] = 255
  }

  // return an SkImage
return image;
}

const image = useDerivedValue(() => createPixels());

Something like this.

tomdoeslinux commented 8 months ago

Ok, thanks

wcandillon commented 8 months ago

Finally thanks to the comments from the Reanimated team, this is the example:

import React from "react";
import { useWindowDimensions } from "react-native";
import {
  AlphaType,
  Canvas,
  ColorType,
  Image,
  Skia,
} from "@shopify/react-native-skia";
import { useDerivedValue, useSharedValue } from "react-native-reanimated";

export const Data = () => {
  const pixels = useSharedValue(new Uint8Array(256 * 256 * 4));
  const { width } = useWindowDimensions();
  const SIZE = width;
  const img = useDerivedValue(() => {
   // If we are not on the UI thread, don't do anything.
    if (!_WORKLET) {
      return;
    }
    // we modify the pixels on the UI thread :)
    pixels.value.fill(255);
    let i = 0;
    for (let x = 0; x < 256 * 4; x++) {
      for (let y = 0; y < 256 * 4; y++) {
        pixels.value[i++] = (x * y) % 255;
      }
    }
    const data = Skia.Data.fromBytes(pixels.value);
    return Skia.Image.MakeImage(
      {
        width: 256,
        height: 256,
        alphaType: AlphaType.Opaque,
        colorType: ColorType.RGBA_8888,
      },
      data,
      256 * 4
    )!;
  });
  return (
    <Canvas style={{ width: SIZE, height: SIZE }}>
      <Image image={img} x={0} y={0} width={256} height={256} fit="cover" />
    </Canvas>
  );
};

I'm closing this issue because based on this solution we will probably never expose the SkBitmap API. I think you have a really cool use-case, you'll probably have more questions you can post on the discussion group, happy hacking!

tomdoeslinux commented 8 months ago

Thanks, do you know how to update it? E.g. when user taps screen. currently I need to recreate whole array but its very expensive

wcandillon commented 8 months ago

yes you can just update the array directly and then to tell reanimated that the value has changed you can use this function: notifyChange(value); The notify change function is available in the react-native-skia package.

On Wed, Feb 7, 2024 at 10:01 AM tomdoeslinux @.***> wrote:

Thanks, do you know how to update it? E.g. when user taps screen. currently I need to recreate whole array but its very expensive

— Reply to this email directly, view it on GitHub or unsubscribe. You are receiving this email because you commented on the thread.

Triage notifications on the go with GitHub Mobile for iOS or Android.

tomdoeslinux commented 8 months ago

Do u have example, its not working for me i get an error

wcandillon commented 8 months ago

Yes this one is a good example where we change values of an existing array and use this function to let reanimated know that something has changed: https://github.com/Shopify/react-native-skia/blob/main/package/src/external/reanimated/buffers.ts#L9

I understand that this is a lot but you will get there, happy hacking 🙌🏼

On Wed, Feb 7, 2024 at 10:39 AM tomdoeslinux @.***> wrote:

Do u have example, its not working for me i get an error

— Reply to this email directly, view it on GitHub or unsubscribe. You are receiving this email because you commented on the thread.

Triage notifications on the go with GitHub Mobile for iOS or Android.

tomdoeslinux commented 8 months ago

Hi,

Sorry if I'm bothering you with all my comments.

I've made a simple example of drawing pixels with reanimated. I'm still learning how to use reanimated, so I might have made some mistakes. I hope my example is good enough to figure out what the problem is or where it's coming from.

The issue is that drawing pixels is slow compared to doing it on Android directly. Don't get me wrong, it's a bit faster than my previous method using state, but it's still not as quick as native Android.

I used performance.now() to benchmark things and find out where the delay is, and it looks like this part of the code is causing a giant bottleneck (I've added data below):

const data = Skia.Data.fromBytes(pixels.value);
    const img = Skia.Image.MakeImage(
      {
        width: size,
        height: size,
        alphaType: AlphaType.Opaque,
        colorType: ColorType.RGBA_8888,
      },
      data,
      size * 4
    )!;

Full code with performance diagnoses:

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

    const PIXEL_MANIP_TIME_START = performance.now();

    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 PIXEL_MANIP_TIME_END = performance.now();
    console.log(`Pixel manipulation execution time: ${PIXEL_MANIP_TIME_END - PIXEL_MANIP_TIME_START} ms`);

    const TIME_IMG_CONVERSION_START = performance.now();

    const data = Skia.Data.fromBytes(pixels.value);
    const img = Skia.Image.MakeImage(
      {
        width: size,
        height: size,
        alphaType: AlphaType.Opaque,
        colorType: ColorType.RGBA_8888,
      },
      data,
      size * 4
    )!;
    const TIME_IMG_CONVERSION_END = performance.now();

    console.log(`Image conversion time: ${TIME_IMG_CONVERSION_END - TIME_IMG_CONVERSION_START} ms`);

    return img
  }, [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',
  },
})

If you just mess around with that, you'll find that the code for converting image into bytes is a significant bottleneck, hence why you could perhaps expose the SkBitmap api.

If you want to even have a better example, change SIZE varaible to something larger like 1000.

Here are some values I got just for general drawing:

 LOG  Pixel manipulation execution time: 0.2815119996666908 ms
 LOG  Image conversion time: 0.5024140030145645 ms
 LOG  Pixel manipulation execution time: 0.2638169974088669 ms
 LOG  Image conversion time: 2.1415360011160374 ms
 LOG  Pixel manipulation execution time: 0.08264600113034248 ms
 LOG  Image conversion time: 1.0859199985861778 ms
 LOG  Pixel manipulation execution time: 0.3380479998886585 ms
 LOG  Image conversion time: 2.6154849976301193 ms
 LOG  Pixel manipulation execution time: 0.17829300090670586 ms
 LOG  Image conversion time: 2.735717996954918 ms
 LOG  Pixel manipulation execution time: 0.20700199902057648 ms
 LOG  Image conversion time: 2.593803998082876 ms
 LOG  Pixel manipulation execution time: 0.09694100171327591 ms
 LOG  Image conversion time: 1.6104019992053509 ms
 LOG  Pixel manipulation execution time: 0.04008299857378006 ms
 LOG  Image conversion time: 4.707642000168562 ms

as you can see, massive difference

Now of course, I could be mistaken with this, because on one hand I don't think 3ms is enough to be noticeable.

But I'm honestly not sure. I do think you were a bit quick to write off not adding a SkBitmap api, as your example that you showed was static, there was no pixel manipulations.


On a sidenote, I would recommend you make it easier for future users and add a section about doing this pixel manipulation. Pixel art applications are a small use case, but image manipulation applications aren't at all, so it would be helpful to add a general part on your docs for manipulating pixels.

I could be mistaken, but I don't know how anyone would figure out how to do what I've done without speaking to library maintainers, it's too complicated, especially compared to native Android. I do understand that it's a bit bleeding edge as well, so I'm fine with acknowledging that.

wcandillon commented 8 months ago

congrats on setting up this example so quickly. The issue here is that in debug mode, the JS engine (Hermes) is extremely slow. In release mode it compiles the JS code to bytecode and in debug mode it runs the example from source. We are not sure if it is expected that debug mode is so much more slower. There might be an issue here.

For instance on my side, a simple 4x4 matrix multiplication in pure JS takes a few milliseconds which is insane considering that it's just an handful of multiplication. Can you try in release mode and see if it matches your expectation? I would be curious to see the figures there.

tomdoeslinux commented 8 months ago

Thanks for the reply.

I tried running the app in Expo production mode as well as compile it fully using npx expo run:android --variant release and there was no difference in speed, still very slow. Unfortunately I can't get the exact figures as console logging is disabled in release modes, but you can feel it lag when you draw.

wcandillon commented 8 months ago

I tried your demo and I am getting good values on both iOS and Android. Android debug mode is notoriously extremely slow.

Screenshot 2024-02-09 at 08 42 07 Screenshot 2024-02-09 at 08 37 22

One small issue I noticed with your demo is that you run the gesture on the JS thread (I think). It should look more like something like this:


  const gesture = Gesture.Pan().onChange((event) => {
    drawLine(pixels.value, { x: 0, y: 0 }, { x: event.x, y: event.y });
    notifyChange(pixels);
  });

  return (
    <GestureHandlerRootView>
      <GestureDetector gesture={gesture}>
        <Canvas style={{ width: SIZE, height: SIZE, backgroundColor: "cyan" }}>
          <Image image={img} x={0} y={0} width={300} height={300} fit="cover" />
        </Canvas>
      </GestureDetector>
    </GestureHandlerRootView>
  );

There might definitely be some optimization possible (maybe there is a better API that could be used, maybe there are some possible optimizations on these existing APIs but there doesn't seem to be a bottleneck yet).

I'm not an expert on pixel art but I'm wondering if it would be worth to draw everything offscreen on a texture (like a regular drawing app) and then use a shader to filter it as pixel art. Here we would never leave the GPU. But I'm not sure what is best yet.

wcandillon commented 8 months ago

it also looks like we can draw the pixels directly without using image conversion (surface.writePixels). We don't expose such method yet but that could be interesting too.

tomdoeslinux commented 8 months ago

I think if you can expose a quicker API, I don't see any harm in adding it to the library. But I've honestly I've kind of given up on using the library and instead switched to using raw WebGL (unfortunately all canvas libraries also don't work, so I'm not left with much of a choice).

I don't really get why I need to use reanimated to update the canvas efficiently, something about it just feels off. There has to be a simpler way. In WebGL you can just pass an ExpoWebGLRenderingContext for batch updates, which just seems more intuitive. I also don't really know how to use reanimated as my code-style is very heavily class-based since I come from an OOP background.

About your approach of using pixel shaders: it does seem pretty interesting, though I am quite sure that the lines/ellipses/general drawing won't be pixel perfect like you see in other pixel art editing apps.

wcandillon commented 8 months ago

using webgl for such a use case is very sensible. you will still need to use reanimated down the road but indeed you can do it nicely step by step.

Did you figure out why I couldn’t see the performance bottleneck on my side?

On Fri 9 Feb 2024 at 09:32, tomdoeslinux @.***> wrote:

I think if you can expose a quicker API, I don't see any harm in adding it to the library, But I've honestly I've kind of given up on using the library and instead switched to using raw WebGL (unfortunately all canvas libraries also don't work, so I'm not left with much of a choice).

I don't really get why I need to use reanimated to update the canvas efficiently, something about it just feels off. In WebGL you can just pass an ExpoWebGLRenderingContext for batch updates, which just seems more intuitive. I also don't really know how to use reanimated as my code-style is very heavily class-based since I come from an OOP background.

About your approach of using pixel shaders: it does seem pretty interesting, though I am quite sure that the lines/ellipses/general drawing won't be pixel perfect like you see in other pixel art editing apps.

— Reply to this email directly, view it on GitHub https://github.com/Shopify/react-native-skia/issues/2199#issuecomment-1935518466 or unsubscribe https://github.com/notifications/unsubscribe-auth/AACKXVXR6KZRIJJHBDW7K4TYSXNJHBFKMF2HI4TJMJ2XIZLTSSBKK5TBNR2WLJDUOJ2WLJDOMFWWLO3UNBZGKYLEL5YGC4TUNFRWS4DBNZ2F6YLDORUXM2LUPGBKK5TBNR2WLJDUOJ2WLJDOMFWWLLTXMF2GG2C7MFRXI2LWNF2HTAVFOZQWY5LFUVUXG43VMWSG4YLNMWVXI2DSMVQWIX3UPFYGLAVFOZQWY5LFVIZTKMRZGIYDEMBWG6SG4YLNMWUWQYLTL5WGCYTFNSWHG5LCNJSWG5C7OR4XAZNMJFZXG5LFINXW23LFNZ2KM5DPOBUWG44TQKSHI6LQMWVHEZLQN5ZWS5DPOJ42K5TBNR2WLKJUGI2TQNBVGMZDLAVEOR4XAZNFNFZXG5LFUV3GC3DVMWVDEMJRGY4DQMZSGM3IFJDUPFYGLJLMMFRGK3FFOZQWY5LFVIZTKMRZGIYDEMBWG6TXI4TJM5TWK4VGMNZGKYLUMU . You are receiving this email because you commented on the thread.

Triage notifications on the go with GitHub Mobile for iOS https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675 or Android https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub .

tomdoeslinux commented 8 months ago

using webgl for such a use case is very sensible. you will still need to use reanimated down the road but indeed you can do it nicely step by step. Did you figure out why I couldn’t see the performance bottleneck on my side? On Fri 9 Feb 2024 at 09:32, tomdoeslinux @.***> wrote: I think if you can expose a quicker API, I don't see any harm in adding it to the library, But I've honestly I've kind of given up on using the library and instead switched to using raw WebGL (unfortunately all canvas libraries also don't work, so I'm not left with much of a choice). I don't really get why I need to use reanimated to update the canvas efficiently, something about it just feels off. In WebGL you can just pass an ExpoWebGLRenderingContext for batch updates, which just seems more intuitive. I also don't really know how to use reanimated as my code-style is very heavily class-based since I come from an OOP background. About your approach of using pixel shaders: it does seem pretty interesting, though I am quite sure that the lines/ellipses/general drawing won't be pixel perfect like you see in other pixel art editing apps. — Reply to this email directly, view it on GitHub <#2199 (comment)> or unsubscribe https://github.com/notifications/unsubscribe-auth/AACKXVXR6KZRIJJHBDW7K4TYSXNJHBFKMF2HI4TJMJ2XIZLTSSBKK5TBNR2WLJDUOJ2WLJDOMFWWLO3UNBZGKYLEL5YGC4TUNFRWS4DBNZ2F6YLDORUXM2LUPGBKK5TBNR2WLJDUOJ2WLJDOMFWWLLTXMF2GG2C7MFRXI2LWNF2HTAVFOZQWY5LFUVUXG43VMWSG4YLNMWVXI2DSMVQWIX3UPFYGLAVFOZQWY5LFVIZTKMRZGIYDEMBWG6SG4YLNMWUWQYLTL5WGCYTFNSWHG5LCNJSWG5C7OR4XAZNMJFZXG5LFINXW23LFNZ2KM5DPOBUWG44TQKSHI6LQMWVHEZLQN5ZWS5DPOJ42K5TBNR2WLKJUGI2TQNBVGMZDLAVEOR4XAZNFNFZXG5LFUV3GC3DVMWVDEMJRGY4DQMZSGM3IFJDUPFYGLJLMMFRGK3FFOZQWY5LFVIZTKMRZGIYDEMBWG6TXI4TJM5TWK4VGMNZGKYLUMU . You are receiving this email because you commented on the thread. Triage notifications on the go with GitHub Mobile for iOS https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675 or Android https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub .

Honestly, not really sure why you didn't see a difference in speeds. Perhaps try to increase bitmap size? You might also need to draw longer strokes. If you just want to recreate what I did, just do a bitmap of size 30 and you will perhaps notice how it's not very fast compared to a native Android Bitmap.

I have an app called 'PixaPencil' which you can download from GitHub as an APK or on F-Droid, perhaps spend some time doing strokes in 'PixaPencil' (or any other native pixel art app on the app store) and compare it to my example app. It was like night'n'day when I compared it myself. Keep in mind Since both Native Android and react native skia use Skia engine, there shouldn't really be much of a speed difference, but there is.

Thanks for the hint that I will need to use reanimated also in WebGl, I would relly appreciate if you could tell me when this would be the case?