Shopify / react-native-skia

High-performance React Native Graphics using Skia

Home Page:https://shopify.github.io/react-native-skia

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Document the relationship between offscreen canvas and pixel density

wcandillon opened this issue · comments

Description

As reported in #1811

Version

latest

Steps to reproduce

Draw an offscreen canvas on a high-density screen. Take a screenshot and compare the image to the same drawing on an onscreen canvas.

Snack, code example, screenshot, or link to a repository

See #1811

CleanShot 2024-04-17 at 18 32 02@2x

I found out those two textures shows different results. (RN Skia v.1.0.2, RN v.0.73.6)

Left: drawing on canvas from offscreen, Right: drawing on canvas from PictureRecoder.

Is this because offscreen doesn't respect pixel density?

yes this is very likely:

// TODO: We're not sure yet why PixelRatio is not needed here.

Could you share the example with me?

@wcandillon Here is the code. You can see the density issue and paint stroke goes different if setting strokeWidth is skipped on the two textures.

I attached the result image (above: offscreen, below: pictureRecorder) and code.

Thanks :)

Versions:
RN skia: 1.1.0
react-native: 0.73.6

CleanShot 2024-04-18 at 13 03 19@2x

import {
  Canvas,
  Group,
  Image,
  Line,
  PaintStyle,
  Picture,
  rect,
  SkCanvas,
  Skia,
  vec,
} from '@shopify/react-native-skia';
import { Dimensions } from 'react-native';
import { useDerivedValue } from 'react-native-reanimated';

const { width: stageWidth, height: stageHeight } = Dimensions.get('screen');
const stageHeightHalf = stageHeight / 2;

const TOTAL = 1000;
const RADIUS = 10;
const BALL_STYLE = Skia.Paint();
BALL_STYLE.setStyle(PaintStyle.Stroke);
// BALL_STYLE.setStrokeWidth(1); // * If you skip setting strokeWidth, the two textures shows different result.
const BALLS = Array.from({ length: TOTAL }, () => ({
  x: RADIUS + Math.random() * (stageWidth - RADIUS),
  y: RADIUS + Math.random() * (stageHeightHalf - RADIUS),
  r: RADIUS,
}));

const Page = () => {
  const drawBalls = (canvas: SkCanvas) => {
    'worklet';

    canvas.save();
    for (let i = 0; i < BALLS.length; i++) {
      const ball = BALLS[i];
      canvas.drawCircle(ball.x, ball.y, ball.r, BALL_STYLE);
    }
    canvas.restore();
  };
  const offscreen = useDerivedValue(() => {
    const offscreen = Skia.Surface.MakeOffscreen(stageWidth, stageHeightHalf)!;
    const canvas = offscreen.getCanvas();
    drawBalls(canvas);
    return offscreen.makeImageSnapshot();
  }, []);
  const pictureRecorder = useDerivedValue(() => {
    const recorder = Skia.PictureRecorder();
    const canvas = recorder.beginRecording(rect(0, 0, stageWidth, stageHeightHalf));
    drawBalls(canvas);
    return recorder.finishRecordingAsPicture();
  }, []);

  return (
    <Canvas
      style={{
        width: stageWidth,
        height: stageHeight,
        backgroundColor: 'lightblue',
      }}
    >
      <Group transform={[{ translate: [0, 0] }]}>
        <Image image={offscreen} width={stageWidth} height={stageHeightHalf} />
      </Group>
      <Group transform={[{ translate: [0, stageHeightHalf] }]}>
        <Picture picture={pictureRecorder} />
      </Group>
      <Group>
        <Line p1={vec(0, stageHeightHalf)} p2={vec(stageWidth, stageHeightHalf)} strokeWidth={10} color="blue" />
      </Group>
    </Canvas>
  );
};

export default Page;

I think this is the expected result.
This is how you need to write the code:

const pd = PixelRatio.get();

  const offscreen = useDerivedValue(() => {
    const off = Skia.Surface.MakeOffscreen(
      stageWidth * pd,
      stageHeightHalf * pd
    )!;
    const canvas = off.getCanvas();
    canvas.scale(pd, pd);
    drawBalls(canvas);
    canvas.restore();
    return off.makeImageSnapshot();
  }, []);

@wcandillon Nice :) Thanks for the insight ! 🥰

@wcandillon Oh I have a question, so you are planning to fix this offscreen density issue to make users don't need to add the pixel density configuration for offscreen canvas?

I don't think it makes sense to add it in this API because the offscreen API has nothing to do with the screen.
But now I am trying to see if this causes inconsistencies with higher-level API (e.g useTexture) which are indeed on screen.

Got it! Thanks 🙏🥰

What we will do as a first step is document this better.

I'm closing it for now, as the current situation seems to make sense.