Shopify / react-native-skia

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

Picture not rerendering on iOS #2732

Open mbpictures opened 1 week ago

mbpictures commented 1 week ago

Hi!

I'm creating a drawing-like component using skia and gesture handler. My code looks like this (simplified):

const DrawBoard = forwardRef<DrawBoardType, Props>(({style}, ref) => {
    const path = useSharedValue<Element>({path: Skia.Path.Make(), paint: Skia.Paint(), type: "draw"});
    const lastTouch = useSharedValue<{x: number; y: number}>({x: 0, y: 0});
    const {toolbar} = useToolbar();
    const currentPicture = useSharedValue(emptyPicture);

    const panGesture = Gesture.Pan()
        .maxPointers(1)
        .minDistance(0)
        .averageTouches(true)
        .enabled(toolbar.activeTool !== Tools.None)
        .onStart(e => {
            const newPath = Skia.Path.Make();
            newPath.moveTo(e.x, e.y);
            if (toolbar.activeTool === Tools.Pencil || toolbar.activeTool === Tools.Eraser) {
                newPath.lineTo(e.x, e.y);
            }
            const newPaint = Skia.Paint();
            newPaint.setStyle(PaintStyle.Stroke);
            newPaint.setColor(Skia.Color(toolbar.color));
            newPaint.setStrokeWidth(toolbar.strokeWidth);
            if (toolbar.activeTool === Tools.Eraser) {
                newPaint.setBlendMode(BlendMode.Clear);
            } else {
                newPaint.setBlendMode(BlendMode.Src);
            }
            path.value = {
                path: newPath,
                paint: newPaint,
                type: toolbar.activeTool === Tools.Eraser ? "erase" : "draw"
            };
            lastTouch.value = {x: e.x, y: e.y};
        })
        .onUpdate(e => {
            if (toolbar.activeTool === Tools.Rectangle) {
                path.value.path.reset();
                path.value.path.addRect({
                    x: lastTouch.value.x,
                    y: lastTouch.value.y,
                    width: e.x - lastTouch.value.x,
                    height: e.y -lastTouch.value.y
                });
            } else if (toolbar.activeTool === Tools.Circle) {
                path.value.path.reset();
                path.value.path.addCircle(lastTouch.value.x, lastTouch.value.y, Math.sqrt(Math.pow(e.x - lastTouch.value.x, 2) + Math.pow(e.y - lastTouch.value.y, 2)));
            } else {
                path.value.path.lineTo(e.x, e.y);
            }
            // update the picture
            currentPicture.value = createPicture(
                canvas => {
                    canvas.drawPath(path.value.path, path.value.paint);
                }
            )
        })
        .onEnd(() => {
            path.value = {path: Skia.Path.Make(), paint: Skia.Paint(), type: "draw"};
        });

    return (
        <View style={[style, {pointerEvents: toolbar.activeTool === Tools.None ? "none" : "auto"}]}>
            <View style={styles.container}>
                <GestureDetector gesture={panGesture}>
                    <Canvas style={styles.canvas}>
                        <Picture picture={currentPicture} />
                    </Canvas>
                </GestureDetector>
            </View>
        </View>
    )
});

This works fine on Android, but on iOS the Picture doesn't rerender. I already added logs inside of the createPicture function and saw, that it is called while painting and also the value of the path looks good, so I have no idea why the picture doesn't rerender.

wcandillon commented 1 week ago

Can you provide a standalone reproducible example? I would be happy to take a look

mbpictures commented 1 week ago

Repro is available here: https://github.com/mbpictures/react-native-skia-sketch-repro