Shopify / react-native-skia

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

Android rendering artefact not working correctly #2418

Open MorneBitcube opened 5 months ago

MorneBitcube commented 5 months ago

Description

I'm using a custom component to render an SVG path with rounded caps. It works fine on iOS. But on Android, it causes a weird render artefact. See image below

1715247013733

1715247090433

This only occurs when 4 or more SVGs in separate canvases are stacked on top of each other

Version

1.2.0

Steps to reproduce

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

import * as React from "react"
import { useEffect } from "react"
import { observer } from "mobx-react-lite"
import { Easing, useSharedValue, withDelay, withTiming } from "react-native-reanimated"
import { Canvas, Color, Path, Skia } from "@shopify/react-native-skia"
import { View } from "react-native"

export type RingSizePreset = keyof typeof $sizePresets

export interface RingProgressProps {
  progress: number
  size: RingSizePreset
  color: Color
  canvasSize: number
}

function degToRad(degrees: number): number {
  return degrees * (Math.PI / 180)
}

export const RingProgress = observer(function RingProgress(props: RingProgressProps) {
  const { progress, size, color, canvasSize } = props

  const { width, startAngleDeg, endAngleDeg } = $sizePresets[size]

  const progressValue = useSharedValue(0)
  useEffect(() => {
    progressValue.value = withDelay(
      100,
      withTiming(progress, { duration: 700, easing: Easing.elastic(1) }),
    )
  }, [progress])

  const strokeWidth = 25
  const center = canvasSize / 2
  const r = (width + canvasSize - strokeWidth * 2) / 2
  const startAngle = degToRad(startAngleDeg)
  const endAngle = degToRad(endAngleDeg)

  const x1 = center - r * Math.cos(endAngle)
  const y1 = -r * Math.sin(endAngle) + center
  const x2 = center - r * Math.cos(startAngle)
  const y2 = -r * Math.sin(startAngle) + center

  const backgroundPath = `M ${x1} ${y1} A ${r} ${r} 0 1 0 ${x2} ${y2}`
  const foregroundPath = `M ${x2} ${y2} A ${r} ${r} 0 1 1 ${x1} ${y1}`

  const skiaBackgroundPath = Skia.Path.MakeFromSVGString(backgroundPath)
  const skiaForegroundPath = Skia.Path.MakeFromSVGString(foregroundPath)

  if (!skiaBackgroundPath || !skiaForegroundPath) {
    return <View />
  }
  return (
    <Canvas
      style={{
        width: canvasSize,
        height: canvasSize,
        position: "absolute",
      }}
    >
      <Path
        path={skiaBackgroundPath}
        strokeCap={"round"}
        strokeWidth={strokeWidth}
        style={"stroke"}
        color={color}
        opacity={0.1}
      />

      <Path
        path={skiaForegroundPath}
        strokeCap={"round"}
        strokeWidth={strokeWidth}
        start={0}
        end={progressValue}
        style={"stroke"}
        color={color}
      />
    </Canvas>
  )
})

const $sizePresets = {
  extraSmall: {
    width: -180,
    startAngleDeg: -70,
    endAngleDeg: 250,
  },
  small: {
    width: -120,
    startAngleDeg: -72,
    endAngleDeg: 252,
  },
medium: { width: -60, startAngleDeg: -74, endAngleDeg: 254 },
  large: { width: 0, startAngleDeg: -76, endAngleDeg: 256 },
}
 export const ProgressRings = () => {

const dataList = [
  { 
    id: 1,
    progress: 0,
    name: "FirstRing",
   size: 'extraSmall',
   color: 'blue'
  },
  {
    id: 2,
    progress: 0.5,
    name: "SecondRing",
    size: 'small',
    color: 'red'
  },
  {
    id: 3,
    progress: 0.5,
    name: "Third",
    size: 'medium',
    color: 'red'
  },
  {
    id: 4,
    progress: 0.5,
    name: "Fourth",
    size: 'large',
    color: 'red'
  },
]

  const $canvasContainer: ViewStyle = {
    width: 400,
    height: 400,
    alignItems: "center",
    justifyContent: "center",
  }

  return (
    <View style={$canvasContainer}>
      {dataList.map((item) => (
        <RingProgress
          color={item.color}
          size={item.size}
          progress={item.progress}
          canvasSize={400}
          key={item.id}
        />
      ))}
    </View>
  )
})
wcandillon commented 5 months ago

Could you provide me with a way to reproduce the issue on my side? A standalone example file would be great. Then we can add it to the test suite.

On Thu, May 9, 2024 at 11:46 AM MorneBitcube @.***> wrote:

Description

I'm using a custom component to render an SVG path with rounded caps. It works fine on iOS. But on Android, it causes a weird render artefact. See image below

1715247013733.jpg (view on web)

1715247090433.jpg (view on web)

This only occurs when 4 or more SVGs in separate canvases are stacked on top of each other

Version

1.2.0

Steps to reproduce

Add the given code to react-native code base and build the app on Android.

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

The ring component

import * as React from "react" import { useEffect } from "react" import { observer } from "mobx-react-lite" import { Easing, useSharedValue, withDelay, withTiming } from "react-native-reanimated" import { Canvas, Color, Path, Skia } from @.***/react-native-skia" import { View } from "react-native"

export type RingSizePreset = keyof typeof $sizePresets

export interface RingProgressProps { progress: number size: RingSizePreset color: Color canvasSize: number }

function degToRad(degrees: number): number { return degrees * (Math.PI / 180) }

export const RingProgress = observer(function RingProgress(props: RingProgressProps) { const { progress, size, color, canvasSize } = props

const { width, startAngleDeg, endAngleDeg } = $sizePresets[size]

const progressValue = useSharedValue(0) useEffect(() => { progressValue.value = withDelay( 100, withTiming(progress, { duration: 700, easing: Easing.elastic(1) }), ) }, [progress])

const strokeWidth = 25 const center = canvasSize / 2 const r = (width + canvasSize - strokeWidth * 2) / 2 const startAngle = degToRad(startAngleDeg) const endAngle = degToRad(endAngleDeg)

const x1 = center - r Math.cos(endAngle) const y1 = -r Math.sin(endAngle) + center const x2 = center - r Math.cos(startAngle) const y2 = -r Math.sin(startAngle) + center

const backgroundPath = M ${x1} ${y1} A ${r} ${r} 0 1 0 ${x2} ${y2} const foregroundPath = M ${x2} ${y2} A ${r} ${r} 0 1 1 ${x1} ${y1}

const skiaBackgroundPath = Skia.Path.MakeFromSVGString(backgroundPath) const skiaForegroundPath = Skia.Path.MakeFromSVGString(foregroundPath)

if (!skiaBackgroundPath || !skiaForegroundPath) { return } return ( <Canvas style={{ width: canvasSize, height: canvasSize, position: "absolute", }}

<Path path={skiaBackgroundPath} strokeCap={"round"} strokeWidth={strokeWidth} style={"stroke"} color={color} opacity={0.1} />

  <Path
    path={skiaForegroundPath}
    strokeCap={"round"}
    strokeWidth={strokeWidth}
    start={0}
    end={progressValue}
    style={"stroke"}
    color={color}
  />
</Canvas>

) })

const $sizePresets = { extraSmall: { width: -180, startAngleDeg: -70, endAngleDeg: 250, }, small: { width: -120, startAngleDeg: -72, endAngleDeg: 252, }, medium: { width: -60, startAngleDeg: -74, endAngleDeg: 254 }, large: { width: 0, startAngleDeg: -76, endAngleDeg: 256 }, }

Implement the component for use

export const ProgressRings = () => {

const dataList = [ { id: 1, progress: 0, name: "FirstRing", size: 'extraSmall', color: 'blue' }, { id: 2, progress: 0.5, name: "SecondRing", size: 'small', color: 'red' }, { id: 3, progress: 0.5, name: "Third", size: 'medium', color: 'red' }, { id: 4, progress: 0.5, name: "Fourth", size: 'large', color: 'red' }, ]

const $canvasContainer: ViewStyle = { width: 400, height: 400, alignItems: "center", justifyContent: "center", }

return (

{dataList.map((item) => ( ))}

) })

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

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

MorneBitcube commented 5 months ago

@wcandillon As requested here is a single file rings.tsx.zip

MorneBitcube commented 5 months ago

@wcandillon have you managed to reproduce this issue yet? I have tried to fix this on my side but was not successful.