diff --git a/evals/skia/01-rn-skia-canvas-fill-background/app/App.tsx b/evals/skia/01-rn-skia-canvas-fill-background/app/App.tsx new file mode 100644 index 0000000..b54b54e --- /dev/null +++ b/evals/skia/01-rn-skia-canvas-fill-background/app/App.tsx @@ -0,0 +1,13 @@ +import { Platform } from 'react-native' + +const FONT_SIZE = 18 + +const fontStyle = { + fontFamily: Platform.select({ ios: 'Helvetica', default: 'serif' }), + fontSize: FONT_SIZE, + fontWeight: 'bold', +} as const + +export default function App() { + return <> +} diff --git a/evals/skia/01-rn-skia-canvas-fill-background/prompt.md b/evals/skia/01-rn-skia-canvas-fill-background/prompt.md new file mode 100644 index 0000000..96c1df7 --- /dev/null +++ b/evals/skia/01-rn-skia-canvas-fill-background/prompt.md @@ -0,0 +1 @@ +Implement a full-screen Skia canvas with a solid background color using Fill. Display the current canvas width and height as centered text inside the canvas. Use useCanvasSize to read the canvas dimensions on the JS thread and position the text accordingly. diff --git a/evals/skia/01-rn-skia-canvas-fill-background/reference/App.tsx b/evals/skia/01-rn-skia-canvas-fill-background/reference/App.tsx new file mode 100644 index 0000000..3bd2bed --- /dev/null +++ b/evals/skia/01-rn-skia-canvas-fill-background/reference/App.tsx @@ -0,0 +1,35 @@ +import { Platform } from 'react-native' +import { Canvas, Fill, Text, matchFont, useCanvasSize } from '@shopify/react-native-skia' + +const FONT_SIZE = 18 + +const fontStyle = { + fontFamily: Platform.select({ ios: 'Helvetica', default: 'serif' }), + fontSize: FONT_SIZE, + fontWeight: 'bold', +} as const + +const font = matchFont(fontStyle) + +function CanvasContent() { + const { width, height } = useCanvasSize() + + const label = `${Math.round(width)} × ${Math.round(height)}` + const textX = width / 2 - (font?.getTextWidth(label) ?? 0) / 2 + const textY = height / 2 + FONT_SIZE / 2 + + return ( + <> + + + + ) +} + +export default function App() { + return ( + + + + ) +} diff --git a/evals/skia/01-rn-skia-canvas-fill-background/requirements.yaml b/evals/skia/01-rn-skia-canvas-fill-background/requirements.yaml new file mode 100644 index 0000000..39e352e --- /dev/null +++ b/evals/skia/01-rn-skia-canvas-fill-background/requirements.yaml @@ -0,0 +1,13 @@ +version: 1 +inputs: + files: + - app/App.tsx +requirements: + - id: uses-canvas-component + description: Must render all drawing content inside a Canvas component from @shopify/react-native-skia. + - id: uses-fill-for-background + description: Must use the Fill component from @shopify/react-native-skia to paint the canvas background color. + - id: uses-canvas-size-hook + description: Must use useCanvasSize from @shopify/react-native-skia to read canvas dimensions on the JS thread. + - id: displays-dimensions-as-text + description: Must render the canvas width and height as visible text inside the canvas using the Text component from @shopify/react-native-skia. diff --git a/evals/skia/02-rn-skia-shape-primitives/app/App.tsx b/evals/skia/02-rn-skia-shape-primitives/app/App.tsx new file mode 100644 index 0000000..47facab --- /dev/null +++ b/evals/skia/02-rn-skia-shape-primitives/app/App.tsx @@ -0,0 +1,12 @@ +import { Canvas, Fill } from '@shopify/react-native-skia' + +const PADDING = 24 +const SHAPE_SIZE = 80 + +export default function App() { + return ( + + + + ) +} diff --git a/evals/skia/02-rn-skia-shape-primitives/prompt.md b/evals/skia/02-rn-skia-shape-primitives/prompt.md new file mode 100644 index 0000000..70ab3a8 --- /dev/null +++ b/evals/skia/02-rn-skia-shape-primitives/prompt.md @@ -0,0 +1 @@ +Draw a composition of basic shapes on a Skia canvas: a filled rectangle, a filled circle, a rounded rectangle, and a straight line. Give each shape a distinct color and arrange them so they are all visible without overlap. diff --git a/evals/skia/02-rn-skia-shape-primitives/reference/App.tsx b/evals/skia/02-rn-skia-shape-primitives/reference/App.tsx new file mode 100644 index 0000000..a3c28a7 --- /dev/null +++ b/evals/skia/02-rn-skia-shape-primitives/reference/App.tsx @@ -0,0 +1,54 @@ +import { + Canvas, + Circle, + Fill, + Line, + Rect, + RoundedRect, + vec, +} from '@shopify/react-native-skia' + +const PADDING = 24 +const SHAPE_SIZE = 80 + +export default function App() { + return ( + + + + + + + + + + + + ) +} diff --git a/evals/skia/02-rn-skia-shape-primitives/requirements.yaml b/evals/skia/02-rn-skia-shape-primitives/requirements.yaml new file mode 100644 index 0000000..a611161 --- /dev/null +++ b/evals/skia/02-rn-skia-shape-primitives/requirements.yaml @@ -0,0 +1,18 @@ +version: 1 +inputs: + files: + - app/App.tsx +requirements: + - id: uses-canvas-component + description: Must render all drawing content inside a Canvas component from @shopify/react-native-skia. + weight: 0.1 + - id: uses-rect-component + description: Must render a Rect component from @shopify/react-native-skia. + - id: uses-circle-component + description: Must render a Circle component from @shopify/react-native-skia. + - id: uses-rounded-rect-component + description: Must render a RoundedRect or equivalent rounded rectangle component from @shopify/react-native-skia. + - id: uses-line-component + description: Must render a Line component from @shopify/react-native-skia. + - id: shapes-have-distinct-colors + description: Each shape must use a visually distinct color. diff --git a/evals/skia/03-rn-skia-path-drawing/app/App.tsx b/evals/skia/03-rn-skia-path-drawing/app/App.tsx new file mode 100644 index 0000000..6988c74 --- /dev/null +++ b/evals/skia/03-rn-skia-path-drawing/app/App.tsx @@ -0,0 +1,11 @@ +import { Canvas, Fill } from '@shopify/react-native-skia' + +const STROKE_WIDTH = 4 + +export default function App() { + return ( + + + + ) +} diff --git a/evals/skia/03-rn-skia-path-drawing/prompt.md b/evals/skia/03-rn-skia-path-drawing/prompt.md new file mode 100644 index 0000000..3722baf --- /dev/null +++ b/evals/skia/03-rn-skia-path-drawing/prompt.md @@ -0,0 +1 @@ +Draw a custom Bezier curve path on a Skia canvas. Build the path imperatively using Skia.Path.Make() with moveTo and cubicTo commands, and render it as a stroked line with a visible stroke width and color. diff --git a/evals/skia/03-rn-skia-path-drawing/reference/App.tsx b/evals/skia/03-rn-skia-path-drawing/reference/App.tsx new file mode 100644 index 0000000..ebaac7f --- /dev/null +++ b/evals/skia/03-rn-skia-path-drawing/reference/App.tsx @@ -0,0 +1,27 @@ +import { Canvas, Fill, Path, Skia } from '@shopify/react-native-skia' + +const STROKE_WIDTH = 4 + +const path = (() => { + const p = Skia.Path.Make() + p.moveTo(40, 300) + p.cubicTo(100, 100, 260, 500, 340, 200) + p.cubicTo(380, 80, 300, 400, 360, 340) + return p +})() + +export default function App() { + return ( + + + + + ) +} diff --git a/evals/skia/03-rn-skia-path-drawing/requirements.yaml b/evals/skia/03-rn-skia-path-drawing/requirements.yaml new file mode 100644 index 0000000..6948925 --- /dev/null +++ b/evals/skia/03-rn-skia-path-drawing/requirements.yaml @@ -0,0 +1,16 @@ +version: 1 +inputs: + files: + - app/App.tsx +requirements: + - id: uses-canvas-component + description: Must render all drawing content inside a Canvas component from @shopify/react-native-skia. + weight: 0.1 + - id: uses-path-component + description: Must render a Path component from @shopify/react-native-skia. + - id: builds-path-imperatively + description: Must build the path using Skia.Path.Make() with at least moveTo and cubicTo or quadTo path commands. + - id: renders-as-stroke + description: Must render the path as a stroke using style="stroke" or strokeWidth property, not as a fill. + - id: path-is-memoized + description: The path object must be created outside of the render function or memoized with useMemo to avoid recreating it on every render. diff --git a/evals/skia/04-rn-skia-paint-stroke-fill/app/App.tsx b/evals/skia/04-rn-skia-paint-stroke-fill/app/App.tsx new file mode 100644 index 0000000..09a8ea0 --- /dev/null +++ b/evals/skia/04-rn-skia-paint-stroke-fill/app/App.tsx @@ -0,0 +1,11 @@ +import { Canvas, Fill } from '@shopify/react-native-skia' + +const SIZE = 300 + +export default function App() { + return ( + + + + ) +} diff --git a/evals/skia/04-rn-skia-paint-stroke-fill/prompt.md b/evals/skia/04-rn-skia-paint-stroke-fill/prompt.md new file mode 100644 index 0000000..100fd41 --- /dev/null +++ b/evals/skia/04-rn-skia-paint-stroke-fill/prompt.md @@ -0,0 +1 @@ +Render a circle that has a solid fill and two concentric strokes of different widths using nested Paint components. Wrap multiple shapes in a Group to demonstrate paint attribute inheritance, where the group-level color is shared by all children. diff --git a/evals/skia/04-rn-skia-paint-stroke-fill/reference/App.tsx b/evals/skia/04-rn-skia-paint-stroke-fill/reference/App.tsx new file mode 100644 index 0000000..7713295 --- /dev/null +++ b/evals/skia/04-rn-skia-paint-stroke-fill/reference/App.tsx @@ -0,0 +1,37 @@ +import { + Canvas, + Circle, + Fill, + Group, + Paint, + Rect, +} from '@shopify/react-native-skia' + +const SIZE = 300 +const CX = SIZE / 2 +const R = 100 +const OUTER_STROKE = 16 +const INNER_STROKE = 8 + +export default function App() { + return ( + + + + + + + + + + + + + + + + + ) +} diff --git a/evals/skia/04-rn-skia-paint-stroke-fill/requirements.yaml b/evals/skia/04-rn-skia-paint-stroke-fill/requirements.yaml new file mode 100644 index 0000000..8df059f --- /dev/null +++ b/evals/skia/04-rn-skia-paint-stroke-fill/requirements.yaml @@ -0,0 +1,14 @@ +version: 1 +inputs: + files: + - app/App.tsx +requirements: + - id: uses-canvas-component + description: Must render all drawing content inside a Canvas component from @shopify/react-native-skia. + weight: 0.1 + - id: uses-paint-children-for-strokes + description: Must use Paint components as children of a drawing element to add at least two strokes with different widths. + - id: paint-uses-stroke-style + description: At least one Paint child must explicitly use style="stroke" with a strokeWidth property. + - id: uses-group-paint-inheritance + description: Must use a Group component with a color or paint prop to demonstrate that paint attributes are inherited by child drawing elements. diff --git a/evals/skia/05-rn-skia-linear-gradient/app/App.tsx b/evals/skia/05-rn-skia-linear-gradient/app/App.tsx new file mode 100644 index 0000000..5bb05d5 --- /dev/null +++ b/evals/skia/05-rn-skia-linear-gradient/app/App.tsx @@ -0,0 +1,6 @@ +const START_COLORS = ['#6366f1', '#3b82f6', '#06b6d4', '#10b981'] +const END_COLORS = ['#f59e0b', '#ef4444', '#ec4899', '#8b5cf6'] + +export default function App() { + return <> +} diff --git a/evals/skia/05-rn-skia-linear-gradient/prompt.md b/evals/skia/05-rn-skia-linear-gradient/prompt.md new file mode 100644 index 0000000..a784ca2 --- /dev/null +++ b/evals/skia/05-rn-skia-linear-gradient/prompt.md @@ -0,0 +1 @@ +Fill a Skia canvas with an animated linear gradient that continuously cycles through two sets of colors. Use LinearGradient as a child of Fill and drive the color interpolation with a Reanimated shared value combined with Skia's interpolateColors function. diff --git a/evals/skia/05-rn-skia-linear-gradient/reference/App.tsx b/evals/skia/05-rn-skia-linear-gradient/reference/App.tsx new file mode 100644 index 0000000..9c47947 --- /dev/null +++ b/evals/skia/05-rn-skia-linear-gradient/reference/App.tsx @@ -0,0 +1,39 @@ +import { useEffect } from 'react' +import { useWindowDimensions } from 'react-native' +import { Canvas, Fill, LinearGradient, interpolateColors, vec } from '@shopify/react-native-skia' +import { useDerivedValue, useSharedValue, withRepeat, withTiming } from 'react-native-reanimated' + +const START_COLORS = ['#6366f1', '#3b82f6', '#06b6d4', '#10b981'] +const END_COLORS = ['#f59e0b', '#ef4444', '#ec4899', '#8b5cf6'] + +const INPUT_RANGE = START_COLORS.map((_, i) => i) + +export default function App() { + const { width, height } = useWindowDimensions() + const progress = useSharedValue(0) + + useEffect(() => { + progress.value = withRepeat( + withTiming(START_COLORS.length - 1, { duration: 3000 }), + -1, + true + ) + }, [progress]) + + const colors = useDerivedValue(() => [ + interpolateColors(progress.value, INPUT_RANGE, START_COLORS), + interpolateColors(progress.value, INPUT_RANGE, END_COLORS), + ]) + + return ( + + + + + + ) +} diff --git a/evals/skia/05-rn-skia-linear-gradient/requirements.yaml b/evals/skia/05-rn-skia-linear-gradient/requirements.yaml new file mode 100644 index 0000000..d89553b --- /dev/null +++ b/evals/skia/05-rn-skia-linear-gradient/requirements.yaml @@ -0,0 +1,16 @@ +version: 1 +inputs: + files: + - app/App.tsx +requirements: + - id: uses-canvas-component + description: Must render all drawing content inside a Canvas component from @shopify/react-native-skia. + weight: 0.1 + - id: uses-linear-gradient-component + description: Must use LinearGradient from @shopify/react-native-skia as a child shader of Fill or another drawing element. + - id: uses-skia-interpolate-colors + description: Must use interpolateColors from @shopify/react-native-skia for color transitions, not interpolateColor from react-native-reanimated. + - id: uses-reanimated-shared-value + description: Must use useSharedValue from react-native-reanimated to drive the gradient animation progress. + - id: animation-is-infinite-loop + description: Must use withRepeat to create a continuous looping color animation. diff --git a/evals/skia/06-rn-skia-radial-gradient/app/App.tsx b/evals/skia/06-rn-skia-radial-gradient/app/App.tsx new file mode 100644 index 0000000..5657f42 --- /dev/null +++ b/evals/skia/06-rn-skia-radial-gradient/app/App.tsx @@ -0,0 +1,11 @@ +import { Canvas, Fill } from '@shopify/react-native-skia' + +const SIZE = 320 + +export default function App() { + return ( + + + + ) +} diff --git a/evals/skia/06-rn-skia-radial-gradient/prompt.md b/evals/skia/06-rn-skia-radial-gradient/prompt.md new file mode 100644 index 0000000..1072ef8 --- /dev/null +++ b/evals/skia/06-rn-skia-radial-gradient/prompt.md @@ -0,0 +1 @@ +Draw a circle filled with a radial gradient using the RadialGradient component as a child shader. The gradient should radiate from the circle's center, transitioning from a bright opaque inner color to a fully transparent outer edge. diff --git a/evals/skia/06-rn-skia-radial-gradient/reference/App.tsx b/evals/skia/06-rn-skia-radial-gradient/reference/App.tsx new file mode 100644 index 0000000..c9b34a5 --- /dev/null +++ b/evals/skia/06-rn-skia-radial-gradient/reference/App.tsx @@ -0,0 +1,29 @@ +import { + Canvas, + Circle, + Fill, + RadialGradient, + vec, +} from '@shopify/react-native-skia' + +const SIZE = 320 +const CX = SIZE / 2 +const CY = SIZE / 2 +const R = 120 + +export default function App() { + return ( + + + + + + + ) +} diff --git a/evals/skia/06-rn-skia-radial-gradient/requirements.yaml b/evals/skia/06-rn-skia-radial-gradient/requirements.yaml new file mode 100644 index 0000000..c2a9059 --- /dev/null +++ b/evals/skia/06-rn-skia-radial-gradient/requirements.yaml @@ -0,0 +1,14 @@ +version: 1 +inputs: + files: + - app/App.tsx +requirements: + - id: uses-canvas-component + description: Must render all drawing content inside a Canvas component from @shopify/react-native-skia. + weight: 0.1 + - id: uses-radial-gradient-component + description: Must use the RadialGradient component from @shopify/react-native-skia as a child of a drawing element. + - id: gradient-center-matches-shape-center + description: The RadialGradient center coordinates (cx, cy) must match the center of the shape it fills. + - id: uses-transparency-in-gradient + description: Must include at least one gradient color stop with an alpha (transparency) value, such as 'transparent' or a color with 0 alpha. diff --git a/evals/skia/07-rn-skia-image-display/app/App.tsx b/evals/skia/07-rn-skia-image-display/app/App.tsx new file mode 100644 index 0000000..07d04f6 --- /dev/null +++ b/evals/skia/07-rn-skia-image-display/app/App.tsx @@ -0,0 +1,14 @@ +import { StyleSheet } from 'react-native' + +const CANVAS_SIZE = 320 +const IMAGE_URL = 'https://picsum.photos/320/320' + +export default function App() { + return <> +} + +const styles = StyleSheet.create({ + container: { + backgroundColor: '#0f172a', + }, +}) diff --git a/evals/skia/07-rn-skia-image-display/prompt.md b/evals/skia/07-rn-skia-image-display/prompt.md new file mode 100644 index 0000000..c370421 --- /dev/null +++ b/evals/skia/07-rn-skia-image-display/prompt.md @@ -0,0 +1 @@ +Load an image using the useImage hook and display it on a Skia canvas using the Image component with the "cover" fit mode. Handle the loading state gracefully by rendering a placeholder rectangle while the image is not yet available. diff --git a/evals/skia/07-rn-skia-image-display/reference/App.tsx b/evals/skia/07-rn-skia-image-display/reference/App.tsx new file mode 100644 index 0000000..28c471d --- /dev/null +++ b/evals/skia/07-rn-skia-image-display/reference/App.tsx @@ -0,0 +1,47 @@ +import { StyleSheet, View } from 'react-native' +import { Canvas, Image, Rect, useImage } from '@shopify/react-native-skia' + +const CANVAS_SIZE = 320 +const IMAGE_URL = 'https://picsum.photos/320/320' + +export default function App() { + const image = useImage(IMAGE_URL) + + return ( + + + {image ? ( + + ) : ( + + )} + + + ) +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: '#0f172a', + }, + canvas: { + width: CANVAS_SIZE, + height: CANVAS_SIZE, + }, +}) diff --git a/evals/skia/07-rn-skia-image-display/requirements.yaml b/evals/skia/07-rn-skia-image-display/requirements.yaml new file mode 100644 index 0000000..3017e33 --- /dev/null +++ b/evals/skia/07-rn-skia-image-display/requirements.yaml @@ -0,0 +1,16 @@ +version: 1 +inputs: + files: + - app/App.tsx +requirements: + - id: uses-canvas-component + description: Must render all drawing content inside a Canvas component from @shopify/react-native-skia. + weight: 0.1 + - id: uses-use-image-hook + description: Must use the useImage hook from @shopify/react-native-skia to load the image, not React Native's Image component. + - id: uses-skia-image-component + description: Must render the Image component from @shopify/react-native-skia (not React Native's built-in Image) to draw the image on the canvas. + - id: uses-cover-fit-mode + description: Must use the fit="cover" prop on the Skia Image component. + - id: handles-null-loading-state + description: Must handle the null state returned by useImage before the image loads, rendering a placeholder or returning null to avoid a crash. diff --git a/evals/skia/08-rn-skia-text-rendering/app/App.tsx b/evals/skia/08-rn-skia-text-rendering/app/App.tsx new file mode 100644 index 0000000..36a3211 --- /dev/null +++ b/evals/skia/08-rn-skia-text-rendering/app/App.tsx @@ -0,0 +1,18 @@ +import { Platform } from 'react-native' +import { Canvas, Fill } from '@shopify/react-native-skia' + +const FONT_SIZE = 18 + +const fontStyle = { + fontFamily: Platform.select({ ios: 'Helvetica', default: 'serif' }), + fontSize: FONT_SIZE, + fontWeight: 'bold', +} as const + +export default function App() { + return ( + + + + ) +} diff --git a/evals/skia/08-rn-skia-text-rendering/prompt.md b/evals/skia/08-rn-skia-text-rendering/prompt.md new file mode 100644 index 0000000..e59d09c --- /dev/null +++ b/evals/skia/08-rn-skia-text-rendering/prompt.md @@ -0,0 +1 @@ +Render a centered line of text on a Skia canvas using the Text component. Resolve the font from the system using matchFont with a bold font weight. Account for the fact that the Text component's y coordinate refers to the text baseline, not the top edge. diff --git a/evals/skia/08-rn-skia-text-rendering/reference/App.tsx b/evals/skia/08-rn-skia-text-rendering/reference/App.tsx new file mode 100644 index 0000000..1c42e5e --- /dev/null +++ b/evals/skia/08-rn-skia-text-rendering/reference/App.tsx @@ -0,0 +1,38 @@ +import { Platform } from 'react-native' +import { + Canvas, + Fill, + Text, + matchFont, + useCanvasSize, +} from '@shopify/react-native-skia' + +const FONT_SIZE = 32 + +const fontStyle = { + fontFamily: Platform.select({ ios: 'Helvetica', default: 'serif' }), + fontSize: FONT_SIZE, + fontWeight: 'bold', +} as const + +const font = matchFont(fontStyle) + +const LABEL = 'Hello, Skia!' + +function CenteredText() { + const { width, height } = useCanvasSize() + const textWidth = font?.getTextWidth(LABEL) ?? 0 + const x = (width - textWidth) / 2 + const y = height / 2 + FONT_SIZE / 2 + + return +} + +export default function App() { + return ( + + + + + ) +} diff --git a/evals/skia/08-rn-skia-text-rendering/requirements.yaml b/evals/skia/08-rn-skia-text-rendering/requirements.yaml new file mode 100644 index 0000000..fb6b7e4 --- /dev/null +++ b/evals/skia/08-rn-skia-text-rendering/requirements.yaml @@ -0,0 +1,16 @@ +version: 1 +inputs: + files: + - app/App.tsx +requirements: + - id: uses-canvas-component + description: Must render all drawing content inside a Canvas component from @shopify/react-native-skia. + weight: 0.1 + - id: uses-match-font + description: Must use matchFont from @shopify/react-native-skia to resolve the font from the system font manager. + - id: font-style-specifies-bold + description: The fontStyle object passed to matchFont must include a bold font weight (fontWeight "bold" or "700" or higher). + - id: uses-text-component + description: Must use the Text component from @shopify/react-native-skia to render the text on the canvas. + - id: text-y-accounts-for-baseline + description: The y value passed to the Text component must account for the fact that y is the text baseline, not the top edge (y should be at least fontSize to be visible). diff --git a/evals/skia/09-rn-skia-blur-filter/app/App.tsx b/evals/skia/09-rn-skia-blur-filter/app/App.tsx new file mode 100644 index 0000000..bcdd2b4 --- /dev/null +++ b/evals/skia/09-rn-skia-blur-filter/app/App.tsx @@ -0,0 +1,24 @@ +import { StyleSheet } from 'react-native' +import { Canvas, Fill } from '@shopify/react-native-skia' + +const BLUR_LEVELS = [0, 2, 6, 14] + +export default function App() { + return ( + + + + ) +} + +const styles = StyleSheet.create({ + container: { + backgroundColor: '#0f172a', + }, + button: { + backgroundColor: '#3b82f6', + }, + buttonText: { + color: '#fff', + }, +}) diff --git a/evals/skia/09-rn-skia-blur-filter/prompt.md b/evals/skia/09-rn-skia-blur-filter/prompt.md new file mode 100644 index 0000000..442d92e --- /dev/null +++ b/evals/skia/09-rn-skia-blur-filter/prompt.md @@ -0,0 +1 @@ +Apply a Gaussian blur image filter to a rectangle on a Skia canvas using the Blur component. Provide a slider or button to adjust the blur intensity at runtime. Render an unblurred element in the background so the blur effect is clearly visible. diff --git a/evals/skia/09-rn-skia-blur-filter/reference/App.tsx b/evals/skia/09-rn-skia-blur-filter/reference/App.tsx new file mode 100644 index 0000000..936c6ee --- /dev/null +++ b/evals/skia/09-rn-skia-blur-filter/reference/App.tsx @@ -0,0 +1,52 @@ +import { useState } from 'react' +import { StyleSheet, Text, TouchableOpacity, View } from 'react-native' +import { Blur, Canvas, Circle, Fill, Rect } from '@shopify/react-native-skia' + +const BLUR_LEVELS = [0, 2, 6, 14] + +export default function App() { + const [blurIndex, setBlurIndex] = useState(1) + const blur = BLUR_LEVELS[blurIndex] + + const cycleBlur = () => setBlurIndex((i) => (i + 1) % BLUR_LEVELS.length) + + return ( + + + + + + + + {blur > 0 && } + + + + + Blur: {blur === 0 ? 'off' : blur} + + + ) +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#0f172a', + }, + canvas: { + flex: 1, + }, + button: { + margin: 24, + paddingVertical: 14, + borderRadius: 10, + backgroundColor: '#3b82f6', + alignItems: 'center', + }, + buttonText: { + color: '#fff', + fontSize: 16, + fontWeight: '600', + }, +}) diff --git a/evals/skia/09-rn-skia-blur-filter/requirements.yaml b/evals/skia/09-rn-skia-blur-filter/requirements.yaml new file mode 100644 index 0000000..7a0bb69 --- /dev/null +++ b/evals/skia/09-rn-skia-blur-filter/requirements.yaml @@ -0,0 +1,14 @@ +version: 1 +inputs: + files: + - app/App.tsx +requirements: + - id: uses-canvas-component + description: Must render all drawing content inside a Canvas component from @shopify/react-native-skia. + weight: 0.1 + - id: uses-blur-component + description: Must use the Blur component from @shopify/react-native-skia as a child image filter of a drawing element or Group. + - id: blur-amount-is-runtime-controllable + description: Must provide a UI control (slider, button, or similar) that changes the blur amount at runtime. + - id: unblurred-background-present + description: Must render at least one element outside of the blurred element so the blur effect is visually distinguishable. diff --git a/evals/skia/10-rn-skia-color-matrix-filter/app/App.tsx b/evals/skia/10-rn-skia-color-matrix-filter/app/App.tsx new file mode 100644 index 0000000..3087505 --- /dev/null +++ b/evals/skia/10-rn-skia-color-matrix-filter/app/App.tsx @@ -0,0 +1,19 @@ +import { StyleSheet } from 'react-native' +import { Canvas, Fill } from '@shopify/react-native-skia' + +export default function App() { + return ( + + + + ) +} + +const styles = StyleSheet.create({ + container: { + backgroundColor: '#f8fafc', + }, + button: { + backgroundColor: '#64748b', + }, +}) diff --git a/evals/skia/10-rn-skia-color-matrix-filter/prompt.md b/evals/skia/10-rn-skia-color-matrix-filter/prompt.md new file mode 100644 index 0000000..cbabf52 --- /dev/null +++ b/evals/skia/10-rn-skia-color-matrix-filter/prompt.md @@ -0,0 +1 @@ +Apply a color matrix filter to a Skia drawing using the ColorMatrix component to desaturate or invert its colors. Use ColorMatrix as a child image filter of a Group or drawing element. Add a button to toggle the filter on and off. diff --git a/evals/skia/10-rn-skia-color-matrix-filter/reference/App.tsx b/evals/skia/10-rn-skia-color-matrix-filter/reference/App.tsx new file mode 100644 index 0000000..cc4adaa --- /dev/null +++ b/evals/skia/10-rn-skia-color-matrix-filter/reference/App.tsx @@ -0,0 +1,64 @@ +import { useState } from 'react' +import { StyleSheet, Text, TouchableOpacity, View } from 'react-native' +import { + Canvas, + Circle, + ColorMatrix, + Fill, + Group, +} from '@shopify/react-native-skia' + +const GRAYSCALE_MATRIX = [ + 0.2126, 0.7152, 0.0722, 0, 0, 0.2126, 0.7152, 0.0722, 0, 0, 0.2126, 0.7152, + 0.0722, 0, 0, 0, 0, 0, 1, 0, +] + +export default function App() { + const [filtered, setFiltered] = useState(false) + + return ( + + + + + + {filtered && } + + + + + + + setFiltered((v) => !v)} + > + + Grayscale: {filtered ? 'ON' : 'OFF'} + + + + ) +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#f8fafc', + }, + canvas: { + flex: 1, + }, + button: { + margin: 24, + paddingVertical: 14, + borderRadius: 10, + backgroundColor: '#64748b', + alignItems: 'center', + }, + buttonText: { + color: '#fff', + fontSize: 16, + fontWeight: '600', + }, +}) diff --git a/evals/skia/10-rn-skia-color-matrix-filter/requirements.yaml b/evals/skia/10-rn-skia-color-matrix-filter/requirements.yaml new file mode 100644 index 0000000..0ed8f29 --- /dev/null +++ b/evals/skia/10-rn-skia-color-matrix-filter/requirements.yaml @@ -0,0 +1,16 @@ +version: 1 +inputs: + files: + - app/App.tsx +requirements: + - id: uses-canvas-component + description: Must render all drawing content inside a Canvas component from @shopify/react-native-skia. + weight: 0.1 + - id: uses-color-matrix-component + description: Must use the ColorMatrix component from @shopify/react-native-skia. + - id: color-matrix-is-child-of-drawing + description: ColorMatrix must be used as a child image filter of a Group, Image, or other drawing element — not as a standalone element. + - id: filter-can-be-toggled + description: Must provide a button or control that enables and disables the ColorMatrix filter at runtime. + - id: matrix-produces-visible-change + description: The color matrix values must produce a noticeable visual transformation (desaturation, inversion, hue rotation, or similar), not an identity matrix. diff --git a/evals/skia/11-rn-skia-reanimated-basic-animation/app/App.tsx b/evals/skia/11-rn-skia-reanimated-basic-animation/app/App.tsx new file mode 100644 index 0000000..f4db03a --- /dev/null +++ b/evals/skia/11-rn-skia-reanimated-basic-animation/app/App.tsx @@ -0,0 +1,12 @@ +import { Canvas, Fill } from '@shopify/react-native-skia' + +const RADIUS = 32 +const DURATION_MS = 1200 + +export default function App() { + return ( + + + + ) +} diff --git a/evals/skia/11-rn-skia-reanimated-basic-animation/prompt.md b/evals/skia/11-rn-skia-reanimated-basic-animation/prompt.md new file mode 100644 index 0000000..732da24 --- /dev/null +++ b/evals/skia/11-rn-skia-reanimated-basic-animation/prompt.md @@ -0,0 +1 @@ +Animate a circle that bounces horizontally across a Skia canvas. Drive the circle's cx position directly with a Reanimated useSharedValue animated with withRepeat and withTiming. Do not use createAnimatedComponent or useAnimatedProps — pass the shared value directly as a Skia prop. diff --git a/evals/skia/11-rn-skia-reanimated-basic-animation/reference/App.tsx b/evals/skia/11-rn-skia-reanimated-basic-animation/reference/App.tsx new file mode 100644 index 0000000..e31d541 --- /dev/null +++ b/evals/skia/11-rn-skia-reanimated-basic-animation/reference/App.tsx @@ -0,0 +1,27 @@ +import { useEffect } from 'react' +import { useWindowDimensions } from 'react-native' +import { Canvas, Circle, Fill } from '@shopify/react-native-skia' +import { useSharedValue, withRepeat, withTiming } from 'react-native-reanimated' + +const RADIUS = 32 +const DURATION_MS = 1200 + +export default function App() { + const { width, height } = useWindowDimensions() + const cx = useSharedValue(RADIUS) + + useEffect(() => { + cx.value = withRepeat( + withTiming(width - RADIUS, { duration: DURATION_MS }), + -1, + true + ) + }, [cx, width]) + + return ( + + + + + ) +} diff --git a/evals/skia/11-rn-skia-reanimated-basic-animation/requirements.yaml b/evals/skia/11-rn-skia-reanimated-basic-animation/requirements.yaml new file mode 100644 index 0000000..40cad57 --- /dev/null +++ b/evals/skia/11-rn-skia-reanimated-basic-animation/requirements.yaml @@ -0,0 +1,16 @@ +version: 1 +inputs: + files: + - app/App.tsx +requirements: + - id: uses-canvas-component + description: Must render all drawing content inside a Canvas component from @shopify/react-native-skia. + weight: 0.1 + - id: shared-value-passed-directly-as-skia-prop + description: Must pass a Reanimated useSharedValue directly as a Skia component property (cx, cy, or r) without wrapping it in createAnimatedComponent or useAnimatedProps. + - id: uses-with-repeat + description: Must use withRepeat from react-native-reanimated to create a looping animation. + - id: uses-with-timing + description: Must use withTiming from react-native-reanimated to control the animation easing and duration. + - id: no-create-animated-component + description: Must not use createAnimatedComponent or useAnimatedProps for the Skia component animation. diff --git a/evals/skia/12-rn-skia-derived-value-animation/app/App.tsx b/evals/skia/12-rn-skia-derived-value-animation/app/App.tsx new file mode 100644 index 0000000..df41e24 --- /dev/null +++ b/evals/skia/12-rn-skia-derived-value-animation/app/App.tsx @@ -0,0 +1,12 @@ +import { Canvas, Fill } from '@shopify/react-native-skia' + +const MIN_R = 20 +const MAX_R = 100 + +export default function App() { + return ( + + + + ) +} diff --git a/evals/skia/12-rn-skia-derived-value-animation/prompt.md b/evals/skia/12-rn-skia-derived-value-animation/prompt.md new file mode 100644 index 0000000..3a86f7b --- /dev/null +++ b/evals/skia/12-rn-skia-derived-value-animation/prompt.md @@ -0,0 +1 @@ +Create a pulsing animation where both the radius and the vertical position of a circle on a Skia canvas change in sync. Derive both Skia properties from a single Reanimated shared value using useDerivedValue, so one animated source drives multiple visual attributes. diff --git a/evals/skia/12-rn-skia-derived-value-animation/reference/App.tsx b/evals/skia/12-rn-skia-derived-value-animation/reference/App.tsx new file mode 100644 index 0000000..9f62ba1 --- /dev/null +++ b/evals/skia/12-rn-skia-derived-value-animation/reference/App.tsx @@ -0,0 +1,38 @@ +import { useEffect } from 'react' +import { useWindowDimensions } from 'react-native' +import { Canvas, Circle, Fill } from '@shopify/react-native-skia' +import { + useDerivedValue, + useSharedValue, + withRepeat, + withTiming, +} from 'react-native-reanimated' + +const MIN_R = 20 +const MAX_R = 100 +const DURATION_MS = 900 + +export default function App() { + const { width, height } = useWindowDimensions() + const progress = useSharedValue(0) + + useEffect(() => { + progress.value = withRepeat( + withTiming(1, { duration: DURATION_MS }), + -1, + true + ) + }, [progress]) + + const r = useDerivedValue(() => MIN_R + (MAX_R - MIN_R) * progress.value) + const cy = useDerivedValue( + () => height / 2 + (MAX_R - MIN_R) * (1 - progress.value) * 0.4 + ) + + return ( + + + + + ) +} diff --git a/evals/skia/12-rn-skia-derived-value-animation/requirements.yaml b/evals/skia/12-rn-skia-derived-value-animation/requirements.yaml new file mode 100644 index 0000000..8aed999 --- /dev/null +++ b/evals/skia/12-rn-skia-derived-value-animation/requirements.yaml @@ -0,0 +1,16 @@ +version: 1 +inputs: + files: + - app/App.tsx +requirements: + - id: uses-canvas-component + description: Must render all drawing content inside a Canvas component from @shopify/react-native-skia. + weight: 0.1 + - id: uses-derived-value + description: Must use useDerivedValue from react-native-reanimated to compute one or more Skia property values from a shared value. + - id: multiple-props-from-single-source + description: At least two different Skia properties (e.g. r and cy) must be derived from the same source shared value. + - id: uses-with-timing-or-spring + description: Must use withTiming or withSpring to drive the source shared value. + - id: animation-loops-continuously + description: Must use withRepeat so the animation runs continuously without stopping. diff --git a/evals/skia/13-rn-skia-animated-color-interpolation/app/App.tsx b/evals/skia/13-rn-skia-animated-color-interpolation/app/App.tsx new file mode 100644 index 0000000..af637b9 --- /dev/null +++ b/evals/skia/13-rn-skia-animated-color-interpolation/app/App.tsx @@ -0,0 +1,5 @@ +const COLORS = ['#6366f1', '#ec4899', '#f59e0b', '#10b981', '#6366f1'] + +export default function App() { + return <> +} diff --git a/evals/skia/13-rn-skia-animated-color-interpolation/prompt.md b/evals/skia/13-rn-skia-animated-color-interpolation/prompt.md new file mode 100644 index 0000000..d65e821 --- /dev/null +++ b/evals/skia/13-rn-skia-animated-color-interpolation/prompt.md @@ -0,0 +1 @@ +Build an animated background that continuously cycles through a sequence of colors using Skia's interpolateColors function. Use useDerivedValue to compute the current color from a Reanimated progress value. Do not use interpolateColor from react-native-reanimated. diff --git a/evals/skia/13-rn-skia-animated-color-interpolation/reference/App.tsx b/evals/skia/13-rn-skia-animated-color-interpolation/reference/App.tsx new file mode 100644 index 0000000..f5c128e --- /dev/null +++ b/evals/skia/13-rn-skia-animated-color-interpolation/reference/App.tsx @@ -0,0 +1,29 @@ +import { useEffect } from 'react' +import { Canvas, Fill, interpolateColors } from '@shopify/react-native-skia' +import { useDerivedValue, useSharedValue, withRepeat, withTiming } from 'react-native-reanimated' + +const COLORS = ['#6366f1', '#ec4899', '#f59e0b', '#10b981', '#6366f1'] +const INPUT_RANGE = COLORS.map((_, i) => i) +const DURATION_MS = 2000 + +export default function App() { + const progress = useSharedValue(0) + + useEffect(() => { + progress.value = withRepeat( + withTiming(COLORS.length - 1, { duration: DURATION_MS }), + -1, + false + ) + }, [progress]) + + const color = useDerivedValue(() => + interpolateColors(progress.value, INPUT_RANGE, COLORS) + ) + + return ( + + + + ) +} diff --git a/evals/skia/13-rn-skia-animated-color-interpolation/requirements.yaml b/evals/skia/13-rn-skia-animated-color-interpolation/requirements.yaml new file mode 100644 index 0000000..51336e4 --- /dev/null +++ b/evals/skia/13-rn-skia-animated-color-interpolation/requirements.yaml @@ -0,0 +1,16 @@ +version: 1 +inputs: + files: + - app/App.tsx +requirements: + - id: uses-canvas-component + description: Must render all drawing content inside a Canvas component from @shopify/react-native-skia. + weight: 0.1 + - id: uses-interpolate-colors-from-skia + description: Must use interpolateColors from @shopify/react-native-skia for color transitions, not interpolateColor from react-native-reanimated. + - id: uses-derived-value-for-color + description: Must use useDerivedValue to compute the animated color value from a shared progress value. + - id: animation-is-infinite + description: Must use withRepeat with a negative count (or equivalent) so the color cycle runs indefinitely. + - id: cycles-through-multiple-colors + description: Must define at least three distinct colors in the input range to produce a multi-step color cycle. diff --git a/evals/skia/14-rn-skia-gesture-pan/app/App.tsx b/evals/skia/14-rn-skia-gesture-pan/app/App.tsx new file mode 100644 index 0000000..1bd40ba --- /dev/null +++ b/evals/skia/14-rn-skia-gesture-pan/app/App.tsx @@ -0,0 +1,11 @@ +import { Canvas, Fill } from '@shopify/react-native-skia' + +const RADIUS = 36 + +export default function App() { + return ( + + + + ) +} diff --git a/evals/skia/14-rn-skia-gesture-pan/prompt.md b/evals/skia/14-rn-skia-gesture-pan/prompt.md new file mode 100644 index 0000000..1d57f00 --- /dev/null +++ b/evals/skia/14-rn-skia-gesture-pan/prompt.md @@ -0,0 +1 @@ +Make a draggable circle on a Skia canvas. Use react-native-gesture-handler's Pan gesture to update Reanimated shared values for the circle's position. Apply withDecay on gesture end to give the circle a physics-based coast-to-stop inertia effect. diff --git a/evals/skia/14-rn-skia-gesture-pan/reference/App.tsx b/evals/skia/14-rn-skia-gesture-pan/reference/App.tsx new file mode 100644 index 0000000..d3025a3 --- /dev/null +++ b/evals/skia/14-rn-skia-gesture-pan/reference/App.tsx @@ -0,0 +1,31 @@ +import { useWindowDimensions } from 'react-native' +import { Canvas, Circle, Fill } from '@shopify/react-native-skia' +import { Gesture, GestureDetector } from 'react-native-gesture-handler' +import { useSharedValue, withDecay } from 'react-native-reanimated' + +const RADIUS = 36 + +export default function App() { + const { width, height } = useWindowDimensions() + const cx = useSharedValue(width / 2) + const cy = useSharedValue(height / 2) + + const gesture = Gesture.Pan() + .onChange((e) => { + cx.value += e.changeX + cy.value += e.changeY + }) + .onEnd((e) => { + cx.value = withDecay({ velocity: e.velocityX, clamp: [RADIUS, width - RADIUS] }) + cy.value = withDecay({ velocity: e.velocityY, clamp: [RADIUS, height - RADIUS] }) + }) + + return ( + + + + + + + ) +} diff --git a/evals/skia/14-rn-skia-gesture-pan/requirements.yaml b/evals/skia/14-rn-skia-gesture-pan/requirements.yaml new file mode 100644 index 0000000..cda86e2 --- /dev/null +++ b/evals/skia/14-rn-skia-gesture-pan/requirements.yaml @@ -0,0 +1,16 @@ +version: 1 +inputs: + files: + - app/App.tsx +requirements: + - id: uses-canvas-component + description: Must render all drawing content inside a Canvas component from @shopify/react-native-skia. + weight: 0.1 + - id: uses-gesture-detector + description: Must wrap the Canvas with GestureDetector from react-native-gesture-handler. + - id: uses-pan-gesture-on-change + description: Must use Gesture.Pan() with an onChange handler that updates position shared values on the UI thread. + - id: uses-with-decay-on-end + description: Must apply withDecay in the Pan gesture onEnd handler to produce inertia-based deceleration after the gesture ends. + - id: circle-position-driven-by-shared-values + description: The draggable circle's cx and cy props must be driven directly by Reanimated shared values. diff --git a/evals/skia/15-rn-skia-transforms/app/App.tsx b/evals/skia/15-rn-skia-transforms/app/App.tsx new file mode 100644 index 0000000..6be6ffb --- /dev/null +++ b/evals/skia/15-rn-skia-transforms/app/App.tsx @@ -0,0 +1,9 @@ +import { Canvas, Fill } from '@shopify/react-native-skia' + +export default function App() { + return ( + + + + ) +} diff --git a/evals/skia/15-rn-skia-transforms/prompt.md b/evals/skia/15-rn-skia-transforms/prompt.md new file mode 100644 index 0000000..a73120f --- /dev/null +++ b/evals/skia/15-rn-skia-transforms/prompt.md @@ -0,0 +1 @@ +Display a set of shapes inside a Skia Group that continuously rotates around the canvas center. Drive the rotation with a Reanimated shared value passed as the transform prop. Nest a second Group inside the rotating group and apply a scale transform to it to demonstrate transform composition. diff --git a/evals/skia/15-rn-skia-transforms/reference/App.tsx b/evals/skia/15-rn-skia-transforms/reference/App.tsx new file mode 100644 index 0000000..c4fcf6b --- /dev/null +++ b/evals/skia/15-rn-skia-transforms/reference/App.tsx @@ -0,0 +1,57 @@ +import { useEffect } from 'react' +import { useWindowDimensions } from 'react-native' +import { Canvas, Circle, Fill, Group, Rect } from '@shopify/react-native-skia' +import { useDerivedValue, useSharedValue, withRepeat, withTiming } from 'react-native-reanimated' + +const DURATION_MS = 3000 + +export default function App() { + const { width, height } = useWindowDimensions() + const cx = width / 2 + const cy = height / 2 + + const angle = useSharedValue(0) + + useEffect(() => { + angle.value = withRepeat(withTiming(Math.PI * 2, { duration: DURATION_MS }), -1, false) + }, [angle]) + + const outerTransform = useDerivedValue(() => [ + { translateX: cx }, + { translateY: cy }, + { rotate: angle.value }, + { translateX: -cx }, + { translateY: -cy }, + ]) + + const innerTransform = useDerivedValue(() => [ + { translateX: cx }, + { translateY: cy }, + { scale: 0.5 }, + { translateX: -cx }, + { translateY: -cy }, + ]) + + return ( + + + + + + + + + + + + + + + ) +} diff --git a/evals/skia/15-rn-skia-transforms/requirements.yaml b/evals/skia/15-rn-skia-transforms/requirements.yaml new file mode 100644 index 0000000..de5fe9c --- /dev/null +++ b/evals/skia/15-rn-skia-transforms/requirements.yaml @@ -0,0 +1,16 @@ +version: 1 +inputs: + files: + - app/App.tsx +requirements: + - id: uses-canvas-component + description: Must render all drawing content inside a Canvas component from @shopify/react-native-skia. + weight: 0.1 + - id: uses-group-with-transform + description: Must use a Group component from @shopify/react-native-skia with a transform prop that includes rotation. + - id: transform-driven-by-reanimated + description: The rotation or transform value must be a Reanimated shared or derived value passed directly as the transform prop. + - id: nested-groups-for-transform-composition + description: Must use at least two nested Group components where the inner group applies a different transform (e.g. scale) from the outer group. + - id: animation-is-continuous + description: The rotation animation must run continuously using withRepeat. diff --git a/evals/skia/16-rn-skia-clip-rect-and-path/app/App.tsx b/evals/skia/16-rn-skia-clip-rect-and-path/app/App.tsx new file mode 100644 index 0000000..5c1b481 --- /dev/null +++ b/evals/skia/16-rn-skia-clip-rect-and-path/app/App.tsx @@ -0,0 +1,12 @@ +import { Canvas, Fill } from '@shopify/react-native-skia' + +const STAR_SVG = + 'M 160 30 L 190 110 L 280 110 L 210 160 L 240 240 L 160 190 L 80 240 L 110 160 L 40 110 L 130 110 Z' + +export default function App() { + return ( + + + + ) +} diff --git a/evals/skia/16-rn-skia-clip-rect-and-path/prompt.md b/evals/skia/16-rn-skia-clip-rect-and-path/prompt.md new file mode 100644 index 0000000..b87d2b2 --- /dev/null +++ b/evals/skia/16-rn-skia-clip-rect-and-path/prompt.md @@ -0,0 +1 @@ +Demonstrate two clipping techniques on a Skia canvas: use ClipRect to restrict a gradient fill to a rectangular region, and use ClipPath to clip a solid-color rectangle to a non-rectangular polygon or star shape defined by an SVG path string. diff --git a/evals/skia/16-rn-skia-clip-rect-and-path/reference/App.tsx b/evals/skia/16-rn-skia-clip-rect-and-path/reference/App.tsx new file mode 100644 index 0000000..29b51c7 --- /dev/null +++ b/evals/skia/16-rn-skia-clip-rect-and-path/reference/App.tsx @@ -0,0 +1,30 @@ +import { Canvas, ClipPath, ClipRect, Fill, Group, LinearGradient, Rect, Skia, vec } from '@shopify/react-native-skia' + +const STAR_SVG = 'M 160 30 L 190 110 L 280 110 L 210 160 L 240 240 L 160 190 L 80 240 L 110 160 L 40 110 L 130 110 Z' +const starPath = Skia.Path.MakeFromSVGString(STAR_SVG)! + +export default function App() { + return ( + + + + {/* ClipRect: restrict a gradient to a rectangle */} + + + + + + + + {/* ClipPath: clip a solid block to a star shape */} + + + + + + ) +} diff --git a/evals/skia/16-rn-skia-clip-rect-and-path/requirements.yaml b/evals/skia/16-rn-skia-clip-rect-and-path/requirements.yaml new file mode 100644 index 0000000..5e437f6 --- /dev/null +++ b/evals/skia/16-rn-skia-clip-rect-and-path/requirements.yaml @@ -0,0 +1,16 @@ +version: 1 +inputs: + files: + - app/App.tsx +requirements: + - id: uses-canvas-component + description: Must render all drawing content inside a Canvas component from @shopify/react-native-skia. + weight: 0.1 + - id: uses-clip-rect + description: Must use the ClipRect component from @shopify/react-native-skia to clip a drawing to a rectangular region. + - id: uses-clip-path + description: Must use the ClipPath component from @shopify/react-native-skia with a path or SVG path string. + - id: clip-path-is-non-rectangular + description: The ClipPath shape must be a non-rectangular polygon, star, or curve — not just a simple rectangle. + - id: both-clips-applied-to-distinct-drawings + description: ClipRect and ClipPath must each be applied to a different visual element so both clipping techniques are demonstrated independently. diff --git a/evals/skia/17-rn-skia-blend-mode/app/App.tsx b/evals/skia/17-rn-skia-blend-mode/app/App.tsx new file mode 100644 index 0000000..189637f --- /dev/null +++ b/evals/skia/17-rn-skia-blend-mode/app/App.tsx @@ -0,0 +1,9 @@ +import { Canvas, Fill } from '@shopify/react-native-skia' + +export default function App() { + return ( + + + + ) +} diff --git a/evals/skia/17-rn-skia-blend-mode/prompt.md b/evals/skia/17-rn-skia-blend-mode/prompt.md new file mode 100644 index 0000000..85df0ff --- /dev/null +++ b/evals/skia/17-rn-skia-blend-mode/prompt.md @@ -0,0 +1 @@ +Render three overlapping circles in cyan, magenta, and yellow arranged like a Venn diagram, placed inside a Skia Group with the multiply blend mode. The overlapping regions should blend into secondary and tertiary colors to demonstrate how Skia blend modes composite layers. diff --git a/evals/skia/17-rn-skia-blend-mode/reference/App.tsx b/evals/skia/17-rn-skia-blend-mode/reference/App.tsx new file mode 100644 index 0000000..8cbc93b --- /dev/null +++ b/evals/skia/17-rn-skia-blend-mode/reference/App.tsx @@ -0,0 +1,21 @@ +import { useWindowDimensions } from 'react-native' +import { Canvas, Circle, Fill, Group } from '@shopify/react-native-skia' + +export default function App() { + const { width, height } = useWindowDimensions() + const cx = width / 2 + const cy = height / 2 + const r = 100 + const offset = 60 + + return ( + + + + + + + + + ) +} diff --git a/evals/skia/17-rn-skia-blend-mode/requirements.yaml b/evals/skia/17-rn-skia-blend-mode/requirements.yaml new file mode 100644 index 0000000..4f99560 --- /dev/null +++ b/evals/skia/17-rn-skia-blend-mode/requirements.yaml @@ -0,0 +1,16 @@ +version: 1 +inputs: + files: + - app/App.tsx +requirements: + - id: uses-canvas-component + description: Must render all drawing content inside a Canvas component from @shopify/react-native-skia. + weight: 0.1 + - id: uses-blend-mode-on-group + description: Must set the blendMode prop on a Group component from @shopify/react-native-skia to composite the child elements. + - id: uses-multiply-blend-mode + description: The blendMode must be "multiply" (or another subtractive/compositing mode such as "screen" or "overlay"), not "normal". + - id: three-overlapping-circles + description: Must render at least three circle elements with overlapping regions so the blend mode visibly affects the intersection areas. + - id: circles-use-primary-colors + description: The circles should use cyan, magenta, and yellow (or equivalent primary colors) to produce recognizable secondary blended colors. diff --git a/evals/skia/18-rn-skia-svg-path-rendering/app/App.tsx b/evals/skia/18-rn-skia-svg-path-rendering/app/App.tsx new file mode 100644 index 0000000..60a72d6 --- /dev/null +++ b/evals/skia/18-rn-skia-svg-path-rendering/app/App.tsx @@ -0,0 +1,12 @@ +import { Canvas, Fill } from '@shopify/react-native-skia' + +const HEART_SVG = + 'M 128 96 C 128 96 80 40 32 72 C -8 96 16 160 64 192 L 128 240 L 192 192 C 240 160 264 96 224 72 C 176 40 128 96 128 96 Z' + +export default function App() { + return ( + + + + ) +} diff --git a/evals/skia/18-rn-skia-svg-path-rendering/prompt.md b/evals/skia/18-rn-skia-svg-path-rendering/prompt.md new file mode 100644 index 0000000..696a94e --- /dev/null +++ b/evals/skia/18-rn-skia-svg-path-rendering/prompt.md @@ -0,0 +1 @@ +Render a heart or star shape on a Skia canvas by parsing an SVG path string with Skia.Path.MakeFromSVGString. Fill the shape with a linear gradient and add a visible stroke outline around it. diff --git a/evals/skia/18-rn-skia-svg-path-rendering/reference/App.tsx b/evals/skia/18-rn-skia-svg-path-rendering/reference/App.tsx new file mode 100644 index 0000000..80a3b64 --- /dev/null +++ b/evals/skia/18-rn-skia-svg-path-rendering/reference/App.tsx @@ -0,0 +1,30 @@ +import { Canvas, Fill, LinearGradient, Path, Skia, vec } from '@shopify/react-native-skia' + +const HEART_SVG = + 'M 128 96 C 128 96 80 40 32 72 C -8 96 16 160 64 192 L 128 240 L 192 192 C 240 160 264 96 224 72 C 176 40 128 96 128 96 Z' + +const heartPath = Skia.Path.MakeFromSVGString(HEART_SVG)! + +export default function App() { + return ( + + + + + + + + + + ) +} diff --git a/evals/skia/18-rn-skia-svg-path-rendering/requirements.yaml b/evals/skia/18-rn-skia-svg-path-rendering/requirements.yaml new file mode 100644 index 0000000..9ff017b --- /dev/null +++ b/evals/skia/18-rn-skia-svg-path-rendering/requirements.yaml @@ -0,0 +1,16 @@ +version: 1 +inputs: + files: + - app/App.tsx +requirements: + - id: uses-canvas-component + description: Must render all drawing content inside a Canvas component from @shopify/react-native-skia. + weight: 0.1 + - id: uses-path-make-from-svg-string + description: Must use Skia.Path.MakeFromSVGString to parse an SVG path string into a SkPath object. + - id: svg-path-is-non-trivial-shape + description: The SVG path string must represent a recognizable non-rectangular shape such as a heart, star, or arrow — not just a simple rectangle or line. + - id: path-has-gradient-fill + description: Must render the path with a LinearGradient or RadialGradient as a child fill shader, not a flat color fill. + - id: path-has-stroke-outline + description: Must render a separate stroked outline of the same path using style="stroke" and a visible strokeWidth. diff --git a/evals/skia/19-rn-skia-runtime-effect-shader/app/App.tsx b/evals/skia/19-rn-skia-runtime-effect-shader/app/App.tsx new file mode 100644 index 0000000..630cffc --- /dev/null +++ b/evals/skia/19-rn-skia-runtime-effect-shader/app/App.tsx @@ -0,0 +1,9 @@ +import { Canvas, Fill } from '@shopify/react-native-skia' + +export default function App() { + return ( + + + + ) +} diff --git a/evals/skia/19-rn-skia-runtime-effect-shader/prompt.md b/evals/skia/19-rn-skia-runtime-effect-shader/prompt.md new file mode 100644 index 0000000..6c3981b --- /dev/null +++ b/evals/skia/19-rn-skia-runtime-effect-shader/prompt.md @@ -0,0 +1 @@ +Write a custom SKSL fragment shader compiled with Skia.RuntimeEffect.Make that generates a procedural color pattern based on UV coordinates. Pass at least one animated uniform value driven by a Reanimated shared value so the shader output changes over time. diff --git a/evals/skia/19-rn-skia-runtime-effect-shader/reference/App.tsx b/evals/skia/19-rn-skia-runtime-effect-shader/reference/App.tsx new file mode 100644 index 0000000..f1f9a38 --- /dev/null +++ b/evals/skia/19-rn-skia-runtime-effect-shader/reference/App.tsx @@ -0,0 +1,41 @@ +import { useEffect } from 'react' +import { useWindowDimensions } from 'react-native' +import { Canvas, Fill, Shader, Skia, vec } from '@shopify/react-native-skia' +import { useDerivedValue, useSharedValue, withRepeat, withTiming } from 'react-native-reanimated' + +const SKSL = ` +uniform float2 resolution; +uniform float time; + +vec4 main(vec2 pos) { + vec2 uv = pos / resolution; + float r = 0.5 + 0.5 * sin(uv.x * 6.28 + time); + float g = 0.5 + 0.5 * sin(uv.y * 6.28 + time + 2.09); + float b = 0.5 + 0.5 * sin((uv.x + uv.y) * 3.14 + time + 4.19); + return vec4(r, g, b, 1.0); +} +` + +const source = Skia.RuntimeEffect.Make(SKSL)! + +export default function App() { + const { width, height } = useWindowDimensions() + const time = useSharedValue(0) + + useEffect(() => { + time.value = withRepeat(withTiming(Math.PI * 2, { duration: 3000 }), -1, false) + }, [time]) + + const uniforms = useDerivedValue(() => ({ + resolution: vec(width, height), + time: time.value, + })) + + return ( + + + + + + ) +} diff --git a/evals/skia/19-rn-skia-runtime-effect-shader/requirements.yaml b/evals/skia/19-rn-skia-runtime-effect-shader/requirements.yaml new file mode 100644 index 0000000..14d8f4c --- /dev/null +++ b/evals/skia/19-rn-skia-runtime-effect-shader/requirements.yaml @@ -0,0 +1,16 @@ +version: 1 +inputs: + files: + - app/App.tsx +requirements: + - id: uses-canvas-component + description: Must render all drawing content inside a Canvas component from @shopify/react-native-skia. + weight: 0.1 + - id: uses-runtime-effect-make + description: Must use Skia.RuntimeEffect.Make to compile a custom SKSL shader string. + - id: shader-used-as-fill-child + description: Must use the Shader component from @shopify/react-native-skia as a child of Fill or another drawing element. + - id: shader-receives-animated-uniform + description: Must pass at least one uniform to the Shader component that is driven by a Reanimated shared or derived value, making the output dynamic. + - id: sksl-uses-uv-or-position + description: The SKSL source must use the pos (or equivalent UV) parameter to produce a position-dependent color pattern. diff --git a/evals/skia/20-rn-skia-canvas-snapshot/app/App.tsx b/evals/skia/20-rn-skia-canvas-snapshot/app/App.tsx new file mode 100644 index 0000000..e0ca32b --- /dev/null +++ b/evals/skia/20-rn-skia-canvas-snapshot/app/App.tsx @@ -0,0 +1,40 @@ +import { StyleSheet, Text, TouchableOpacity, View } from 'react-native' +import { Canvas, Fill } from '@shopify/react-native-skia' + +export default function App() { + const handleSave = () => {} + + return ( + + + + + + + Save Snapshot + + + ) +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#0f172a', + }, + canvas: { + flex: 1, + }, + button: { + margin: 24, + paddingVertical: 14, + borderRadius: 10, + backgroundColor: '#3b82f6', + alignItems: 'center', + }, + buttonText: { + color: '#fff', + fontSize: 16, + fontWeight: '600', + }, +}) diff --git a/evals/skia/20-rn-skia-canvas-snapshot/prompt.md b/evals/skia/20-rn-skia-canvas-snapshot/prompt.md new file mode 100644 index 0000000..e0b5669 --- /dev/null +++ b/evals/skia/20-rn-skia-canvas-snapshot/prompt.md @@ -0,0 +1 @@ +Implement a Skia canvas with a drawing, and add a Save button that captures the canvas content as an image using makeImageSnapshot via a canvas ref. Call encodeToBytes on the result and log the byte length to confirm the snapshot was taken successfully. diff --git a/evals/skia/20-rn-skia-canvas-snapshot/reference/App.tsx b/evals/skia/20-rn-skia-canvas-snapshot/reference/App.tsx new file mode 100644 index 0000000..856bb96 --- /dev/null +++ b/evals/skia/20-rn-skia-canvas-snapshot/reference/App.tsx @@ -0,0 +1,53 @@ +import { StyleSheet, Text, TouchableOpacity, View } from 'react-native' +import { Canvas, Circle, Fill, useCanvasRef } from '@shopify/react-native-skia' + +export default function App() { + const ref = useCanvasRef() + + const handleSave = () => { + const image = ref.current?.makeImageSnapshot() + if (!image) { + console.warn('Snapshot failed: canvas ref not ready') + return + } + const bytes = image.encodeToBytes() + console.log('Snapshot byte length:', bytes.length) + } + + return ( + + + + + + + + + + Save Snapshot + + + ) +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#0f172a', + }, + canvas: { + flex: 1, + }, + button: { + margin: 24, + paddingVertical: 14, + borderRadius: 10, + backgroundColor: '#3b82f6', + alignItems: 'center', + }, + buttonText: { + color: '#fff', + fontSize: 16, + fontWeight: '600', + }, +}) diff --git a/evals/skia/20-rn-skia-canvas-snapshot/requirements.yaml b/evals/skia/20-rn-skia-canvas-snapshot/requirements.yaml new file mode 100644 index 0000000..65bddff --- /dev/null +++ b/evals/skia/20-rn-skia-canvas-snapshot/requirements.yaml @@ -0,0 +1,16 @@ +version: 1 +inputs: + files: + - app/App.tsx +requirements: + - id: uses-canvas-component + description: Must render all drawing content inside a Canvas component from @shopify/react-native-skia. + weight: 0.1 + - id: uses-canvas-ref + description: Must use useCanvasRef from @shopify/react-native-skia to obtain a ref to the Canvas component. + - id: calls-make-image-snapshot + description: Must call makeImageSnapshot() on the canvas ref object to capture the current canvas drawing as an SkImage. + - id: encodes-image-to-bytes + description: Must call encodeToBytes() or encodeToBase64() on the resulting SkImage to produce a usable data representation. + - id: snapshot-triggered-by-user-action + description: The snapshot must be triggered by a user action such as pressing a button, not automatically on mount. diff --git a/evals/skia/21-rn-skia-sweep-gradient/app/App.tsx b/evals/skia/21-rn-skia-sweep-gradient/app/App.tsx new file mode 100644 index 0000000..b5c6d0f --- /dev/null +++ b/evals/skia/21-rn-skia-sweep-gradient/app/App.tsx @@ -0,0 +1,14 @@ +import { Canvas, Fill } from '@shopify/react-native-skia' + +const SIZE = 320 +const COLORS = ['#06b6d4', '#8b5cf6', '#f472b6', '#06b6d4'] + +export default function App() { + return ( + + + + ) +} diff --git a/evals/skia/21-rn-skia-sweep-gradient/prompt.md b/evals/skia/21-rn-skia-sweep-gradient/prompt.md new file mode 100644 index 0000000..bb30996 --- /dev/null +++ b/evals/skia/21-rn-skia-sweep-gradient/prompt.md @@ -0,0 +1 @@ +Fill a circle on a Skia canvas with a SweepGradient shader that cycles through at least three distinct colors around the center point. The gradient center must match the circle center so the sweep reads as a radial color wheel or gauge segment. diff --git a/evals/skia/21-rn-skia-sweep-gradient/reference/App.tsx b/evals/skia/21-rn-skia-sweep-gradient/reference/App.tsx new file mode 100644 index 0000000..9d07e02 --- /dev/null +++ b/evals/skia/21-rn-skia-sweep-gradient/reference/App.tsx @@ -0,0 +1,26 @@ +import { + Canvas, + Circle, + Fill, + SweepGradient, + vec, +} from '@shopify/react-native-skia' + +const SIZE = 320 +const CX = SIZE / 2 +const CY = SIZE / 2 +const R = 120 +const COLORS = ['#06b6d4', '#8b5cf6', '#f472b6', '#06b6d4'] + +export default function App() { + return ( + + + + + + + ) +} diff --git a/evals/skia/21-rn-skia-sweep-gradient/requirements.yaml b/evals/skia/21-rn-skia-sweep-gradient/requirements.yaml new file mode 100644 index 0000000..c1b655a --- /dev/null +++ b/evals/skia/21-rn-skia-sweep-gradient/requirements.yaml @@ -0,0 +1,14 @@ +version: 1 +inputs: + files: + - app/App.tsx +requirements: + - id: uses-canvas-component + description: Must render all drawing content inside a Canvas component from @shopify/react-native-skia. + weight: 0.1 + - id: uses-sweep-gradient-component + description: Must use the SweepGradient component from @shopify/react-native-skia as a child shader of a drawing element. + - id: sweep-has-multiple-colors + description: The SweepGradient colors array must include at least three distinct color stops to produce a visible angular color cycle. + - id: sweep-center-matches-shape-center + description: The SweepGradient c (center) coordinates must match the center of the shape it fills. diff --git a/evals/skia/22-rn-skia-group-layer-effect/app/App.tsx b/evals/skia/22-rn-skia-group-layer-effect/app/App.tsx new file mode 100644 index 0000000..315b62a --- /dev/null +++ b/evals/skia/22-rn-skia-group-layer-effect/app/App.tsx @@ -0,0 +1,15 @@ +import { Canvas, Fill } from '@shopify/react-native-skia' + +const SIZE = 320 +const CY = 160 + +const COLOR1 = '#6366f1' +const COLOR2 = '#ec4899' + +export default function App() { + return ( + + + + ) +} diff --git a/evals/skia/22-rn-skia-group-layer-effect/prompt.md b/evals/skia/22-rn-skia-group-layer-effect/prompt.md new file mode 100644 index 0000000..377b81c --- /dev/null +++ b/evals/skia/22-rn-skia-group-layer-effect/prompt.md @@ -0,0 +1 @@ +Draw at least two overlapping shapes inside a Skia Group and apply a blur to their combined composite using the Group layer prop. Pass a Paint element with a Blur child to layer — do not attach Blur directly to each shape individually. diff --git a/evals/skia/22-rn-skia-group-layer-effect/reference/App.tsx b/evals/skia/22-rn-skia-group-layer-effect/reference/App.tsx new file mode 100644 index 0000000..f61c8aa --- /dev/null +++ b/evals/skia/22-rn-skia-group-layer-effect/reference/App.tsx @@ -0,0 +1,34 @@ +import { + Blur, + Canvas, + Circle, + Fill, + Group, + Paint, +} from '@shopify/react-native-skia' + +const SIZE = 320 +const CY = 160 + +const COLOR1 = '#6366f1' +const COLOR2 = '#ec4899' + +export default function App() { + return ( + + + + + + } + > + + + + + ) +} diff --git a/evals/skia/22-rn-skia-group-layer-effect/requirements.yaml b/evals/skia/22-rn-skia-group-layer-effect/requirements.yaml new file mode 100644 index 0000000..6cd07e0 --- /dev/null +++ b/evals/skia/22-rn-skia-group-layer-effect/requirements.yaml @@ -0,0 +1,14 @@ +version: 1 +inputs: + files: + - app/App.tsx +requirements: + - id: uses-canvas-component + description: Must render all drawing content inside a Canvas component from @shopify/react-native-skia. + weight: 0.1 + - id: uses-group-layer-prop + description: Must set the layer prop on a Group component from @shopify/react-native-skia. + - id: layer-uses-paint-with-blur + description: The layer prop must be a Paint element that contains a Blur image filter as a child. + - id: group-has-multiple-shapes + description: The Group with the layer effect must contain at least two drawing elements (e.g. circles or rects) so the effect applies to a composite, not a single shape. diff --git a/evals/skia/23-rn-skia-paragraph-styled-text/app/App.tsx b/evals/skia/23-rn-skia-paragraph-styled-text/app/App.tsx new file mode 100644 index 0000000..c41881c --- /dev/null +++ b/evals/skia/23-rn-skia-paragraph-styled-text/app/App.tsx @@ -0,0 +1,11 @@ +import { Canvas, Fill } from '@shopify/react-native-skia' + +const PARAGRAPH_WIDTH = 300 + +export default function App() { + return ( + + + + ) +} diff --git a/evals/skia/23-rn-skia-paragraph-styled-text/prompt.md b/evals/skia/23-rn-skia-paragraph-styled-text/prompt.md new file mode 100644 index 0000000..635a188 --- /dev/null +++ b/evals/skia/23-rn-skia-paragraph-styled-text/prompt.md @@ -0,0 +1 @@ +Render a multi-line text block on a Skia canvas using the Paragraph component and Skia.ParagraphBuilder. Apply at least two distinct text styles within the same paragraph (for example bold title plus regular subtitle) using pushStyle and pop, and center-align the paragraph with TextAlign. diff --git a/evals/skia/23-rn-skia-paragraph-styled-text/reference/App.tsx b/evals/skia/23-rn-skia-paragraph-styled-text/reference/App.tsx new file mode 100644 index 0000000..c1f1a5f --- /dev/null +++ b/evals/skia/23-rn-skia-paragraph-styled-text/reference/App.tsx @@ -0,0 +1,47 @@ +import { useMemo } from 'react' +import { useWindowDimensions } from 'react-native' +import { + Canvas, + Fill, + FontStyle, + Paragraph, + Skia, + TextAlign, +} from '@shopify/react-native-skia' + +const PARAGRAPH_WIDTH = 300 + +export default function App() { + const { width } = useWindowDimensions() + + const paragraph = useMemo(() => { + const para = Skia.ParagraphBuilder.Make({ textAlign: TextAlign.Center }) + .pushStyle({ + fontSize: 28, + color: Skia.Color('#f8fafc'), + fontStyle: FontStyle.Bold, + }) + .addText('React Native Skia\n') + .pop() + .pushStyle({ + fontSize: 16, + color: Skia.Color('#94a3b8'), + fontStyle: FontStyle.Normal, + }) + .addText('Paragraph API with mixed styles') + .build() + + para.layout(PARAGRAPH_WIDTH) + return para + }, []) + + const x = (width - PARAGRAPH_WIDTH) / 2 + const y = 120 + + return ( + + + + + ) +} diff --git a/evals/skia/23-rn-skia-paragraph-styled-text/requirements.yaml b/evals/skia/23-rn-skia-paragraph-styled-text/requirements.yaml new file mode 100644 index 0000000..cfaaa31 --- /dev/null +++ b/evals/skia/23-rn-skia-paragraph-styled-text/requirements.yaml @@ -0,0 +1,16 @@ +version: 1 +inputs: + files: + - app/App.tsx +requirements: + - id: uses-canvas-component + description: Must render all drawing content inside a Canvas component from @shopify/react-native-skia. + weight: 0.1 + - id: uses-paragraph-component + description: Must render text using the Paragraph component from @shopify/react-native-skia, not the low-level Text component alone. + - id: uses-paragraph-builder + description: Must build the paragraph with Skia.ParagraphBuilder.Make and chain addText calls. + - id: multiple-style-segments + description: Must apply at least two different text styles in the same paragraph using pushStyle and pop (e.g. different font sizes, weights, or colors). + - id: paragraph-uses-text-align + description: Must set a paragraph-level textAlign style such as TextAlign.Center on the ParagraphBuilder.Make options. diff --git a/evals/skia/24-rn-skia-picture-save-restore/app/App.tsx b/evals/skia/24-rn-skia-picture-save-restore/app/App.tsx new file mode 100644 index 0000000..0ce4074 --- /dev/null +++ b/evals/skia/24-rn-skia-picture-save-restore/app/App.tsx @@ -0,0 +1,19 @@ +import { Canvas, Fill } from '@shopify/react-native-skia' + +const SIZE = 320 +const BACKGROUND_COLOR = '#0f172a' +const FIRST_RECT_COLOR = '#38bdf8' +const AXIS_ALIGNED_RECT_COLOR = '#f472b6' +const THIRD_RECT_COLOR = '#4ade80' +const FIRST_RECT_ROTATION_DEG = 45 +const THIRD_RECT_ROTATION_DEG = -23 + +export default function App() { + return ( + + + + ) +} diff --git a/evals/skia/24-rn-skia-picture-save-restore/prompt.md b/evals/skia/24-rn-skia-picture-save-restore/prompt.md new file mode 100644 index 0000000..455e49e --- /dev/null +++ b/evals/skia/24-rn-skia-picture-save-restore/prompt.md @@ -0,0 +1 @@ +Record a Skia Picture with Skia.PictureRecorder and draw three rectangles on the imperative canvas: first rotated 45 degrees, then one axis-aligned at 0 degrees, then one rotated -23 degrees. Use the imperative canvas API to apply the transforms and draw the rectangles. diff --git a/evals/skia/24-rn-skia-picture-save-restore/reference/App.tsx b/evals/skia/24-rn-skia-picture-save-restore/reference/App.tsx new file mode 100644 index 0000000..a856677 --- /dev/null +++ b/evals/skia/24-rn-skia-picture-save-restore/reference/App.tsx @@ -0,0 +1,57 @@ +import { useMemo } from 'react' +import { Canvas, Fill, Picture, Skia } from '@shopify/react-native-skia' + +const SIZE = 320 +const CX = SIZE / 2 +const BACKGROUND_COLOR = '#0f172a' +const FIRST_RECT_COLOR = '#38bdf8' +const AXIS_ALIGNED_RECT_COLOR = '#f472b6' +const THIRD_RECT_COLOR = '#4ade80' +const FIRST_RECT_ROTATION_DEG = 45 +const THIRD_RECT_ROTATION_DEG = -23 +const RECT_WIDTH = 120 +const RECT_HEIGHT = 48 + +const paint = Skia.Paint() +paint.setAntiAlias(true) + +export default function App() { + const picture = useMemo(() => { + const recorder = Skia.PictureRecorder() + const canvas = recorder.beginRecording(Skia.XYWHRect(0, 0, SIZE, SIZE)) + + paint.setColor(Skia.Color(FIRST_RECT_COLOR)) + canvas.save() + canvas.translate(CX, 90) + canvas.rotate(FIRST_RECT_ROTATION_DEG) + canvas.drawRect( + { x: -RECT_WIDTH / 2, y: -RECT_HEIGHT / 2, width: RECT_WIDTH, height: RECT_HEIGHT }, + paint + ) + canvas.restore() + + paint.setColor(Skia.Color(AXIS_ALIGNED_RECT_COLOR)) + canvas.drawRect({ x: 40, y: 144, width: 240, height: 32 }, paint) + + paint.setColor(Skia.Color(THIRD_RECT_COLOR)) + canvas.save() + canvas.translate(CX, 230) + canvas.rotate(THIRD_RECT_ROTATION_DEG) + canvas.drawRect( + { x: -RECT_WIDTH / 2, y: -RECT_HEIGHT / 2, width: RECT_WIDTH, height: RECT_HEIGHT }, + paint + ) + canvas.restore() + + return recorder.finishRecordingAsPicture() + }, []) + + return ( + + + + + ) +} diff --git a/evals/skia/24-rn-skia-picture-save-restore/requirements.yaml b/evals/skia/24-rn-skia-picture-save-restore/requirements.yaml new file mode 100644 index 0000000..302e348 --- /dev/null +++ b/evals/skia/24-rn-skia-picture-save-restore/requirements.yaml @@ -0,0 +1,20 @@ +version: 1 +inputs: + files: + - app/App.tsx +requirements: + - id: uses-canvas-component + description: Must render all drawing content inside a Canvas component from @shopify/react-native-skia. + weight: 0.1 + - id: uses-picture-recorder + description: Must create the drawing with Skia.PictureRecorder using beginRecording and finishRecordingAsPicture. + - id: uses-picture-component + description: Must render the recorded picture with the Picture component from @shopify/react-native-skia. + - id: uses-canvas-save-and-restore + description: Must call canvas.save() before each rotated draw and canvas.restore() after it, as paired stack operations. + - id: first-rect-rotated-45-degrees + description: The first rectangle must be drawn inside a save/restore block with a 45 degree rotation applied before the draw call. + - id: second-rect-axis-aligned + description: The second rectangle must be drawn without an active rotation transform — axis-aligned at 0 degrees between the two rotated rectangles. + - id: third-rect-rotated-minus-23-degrees + description: The third rectangle must be drawn inside a save/restore block with a -23 degree rotation applied before the draw call. diff --git a/evals/skia/25-rn-skia-lottie-playback/app/App.tsx b/evals/skia/25-rn-skia-lottie-playback/app/App.tsx new file mode 100644 index 0000000..c1c5102 --- /dev/null +++ b/evals/skia/25-rn-skia-lottie-playback/app/App.tsx @@ -0,0 +1,33 @@ +import { Canvas, Fill } from '@shopify/react-native-skia' + +const CANVAS_SIZE = 300 +const BACKGROUND_COLOR = '#0f172a' + +export const LOTTIE_JSON = { + v: '5.5.7', + fr: 30, + ip: 0, + op: 90, + w: 300, + h: 300, + nm: 'Pulsing Circle', + ddd: 0, + /** + * Assume this asset variable is complete. + */ +} + +export default function App() { + return ( + + + + ) +} diff --git a/evals/skia/25-rn-skia-lottie-playback/prompt.md b/evals/skia/25-rn-skia-lottie-playback/prompt.md new file mode 100644 index 0000000..5e13738 --- /dev/null +++ b/evals/skia/25-rn-skia-lottie-playback/prompt.md @@ -0,0 +1 @@ +Load the provided Lottie JSON with Skia.Skottie.Make(JSON.stringify(...)) and render it on a Skia canvas using the Skottie component. Drive looping playback by deriving the current frame from useClock with useDerivedValue, using the animation fps() and duration() to wrap the frame index. Assume the given Lottie JSON resource variable (LOTTIE_JSON) is complete. diff --git a/evals/skia/25-rn-skia-lottie-playback/reference/App.tsx b/evals/skia/25-rn-skia-lottie-playback/reference/App.tsx new file mode 100644 index 0000000..3db46a6 --- /dev/null +++ b/evals/skia/25-rn-skia-lottie-playback/reference/App.tsx @@ -0,0 +1,58 @@ +import { useMemo } from 'react' +import { + Canvas, + Fill, + Skia, + Skottie, + useClock, +} from '@shopify/react-native-skia' +import { useDerivedValue } from 'react-native-reanimated' + +const CANVAS_SIZE = 300 +const BACKGROUND_COLOR = '#0f172a' + +const LOTTIE_JSON = { + v: '5.5.7', + fr: 30, + ip: 0, + op: 90, + w: 300, + h: 300, + nm: 'Pulsing Circle', + ddd: 0, + /** + * Assume this asset variable is complete. + */ +} + +export default function App() { + const animation = useMemo( + () => Skia.Skottie.Make(JSON.stringify(LOTTIE_JSON)), + [] + ) + + const clock = useClock() + const frame = useDerivedValue(() => { + if (!animation) { + return 0 + } + + const fps = animation.fps() + const duration = animation.duration() + return Math.floor((clock.value / 1000) * fps) % Math.floor(duration * fps) + }) + + return ( + + + {animation ? : null} + + ) +} diff --git a/evals/skia/25-rn-skia-lottie-playback/requirements.yaml b/evals/skia/25-rn-skia-lottie-playback/requirements.yaml new file mode 100644 index 0000000..99b26ff --- /dev/null +++ b/evals/skia/25-rn-skia-lottie-playback/requirements.yaml @@ -0,0 +1,18 @@ +version: 1 +inputs: + files: + - app/App.tsx +requirements: + - id: uses-canvas-component + description: Must render all drawing content inside a Canvas component from @shopify/react-native-skia. + weight: 0.1 + - id: uses-skottie-make + description: Must create the animation with Skia.Skottie.Make, passing the Lottie JSON via JSON.stringify. + - id: uses-skottie-component + description: Must render the Skottie component from @shopify/react-native-skia with the created animation and a frame prop. + - id: uses-use-clock + description: Must use the useClock hook from @shopify/react-native-skia to drive playback timing. + - id: uses-derived-value-for-frame + description: Must compute the Skottie frame prop with useDerivedValue from react-native-reanimated, reading the clock shared value. + - id: frame-loops-with-duration-and-fps + description: Must wrap the derived frame index with modulo using the animation fps() and duration() so playback loops continuously. diff --git a/evals/skia/README.md b/evals/skia/README.md new file mode 100644 index 0000000..3b18ea2 --- /dev/null +++ b/evals/skia/README.md @@ -0,0 +1,100 @@ +# Skia eval category + +React Native Skia evals — testing how well LLMs implement high-performance 2D drawing using `@shopify/react-native-skia`. + +## Official docs + +- Canvas overview: https://shopify.github.io/react-native-skia/docs/canvas/overview +- Painting: https://shopify.github.io/react-native-skia/docs/paint/overview +- Shapes: https://shopify.github.io/react-native-skia/docs/shapes/rect +- Path: https://shopify.github.io/react-native-skia/docs/shapes/path +- Text: https://shopify.github.io/react-native-skia/docs/text/text +- Paragraph: https://shopify.github.io/react-native-skia/docs/text/paragraph +- Group: https://shopify.github.io/react-native-skia/docs/group +- Images: https://shopify.github.io/react-native-skia/docs/images +- Image filters: https://shopify.github.io/react-native-skia/docs/image-filters/overview +- Shaders: https://shopify.github.io/react-native-skia/docs/shaders/overview +- Gradients: https://shopify.github.io/react-native-skia/docs/shaders/gradients +- Pictures: https://shopify.github.io/react-native-skia/docs/shapes/pictures +- Mask: https://shopify.github.io/react-native-skia/docs/mask +- Animations: https://shopify.github.io/react-native-skia/docs/animations/animations +- Gestures: https://shopify.github.io/react-native-skia/docs/animations/gestures +- Skottie: https://shopify.github.io/react-native-skia/docs/skottie + +## Best-practice inventory + +| # | Rule | Source | +| --- | --------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | +| 1 | Render all Skia drawing inside a `` root component | canvas/overview | +| 2 | Pass Reanimated `useSharedValue` / `useDerivedValue` directly as Skia props — no `createAnimatedComponent` or `useAnimatedProps` needed | animations/animations | +| 3 | Use `interpolateColors` from `@shopify/react-native-skia`, not `interpolateColor` from Reanimated, for color transitions | animations/animations | +| 4 | Wrap gesture handlers around the `` using `GestureDetector`; for per-element gesture tracking, overlay an `Animated.View` | animations/gestures | +| 5 | Use `useCanvasSize` (JS thread) or the `onSize` shared value prop (UI thread) to read canvas dimensions reactively | canvas/overview | +| 6 | Build paths imperatively with `Skia.Path.Make()` or parse SVG strings with `Skia.Path.MakeFromSVGString` | shapes/path | +| 7 | Use `Paint` children on drawing elements for multiple fills/strokes; inherit paint attributes via `Group` | paint/overview | +| 8 | Compose image filters by nesting `Blur`, `ColorMatrix`, etc. as children of the target drawing element or `Group` | image-filters/overview | +| 9 | Compile custom SKSL shaders with `Skia.RuntimeEffect.Make`; use the `Shader` component as a child of `Fill` | shaders/overview | +| 10 | Capture canvas output with `makeImageSnapshot()` via `useCanvasRef`; call `encodeToBytes()` for raw pixel data | canvas/overview | +| 11 | Use `matchFont` with a `fontStyle` object for system font resolution; the Text `y` origin is the text baseline, not the top | text/text | +| 12 | Apply blend modes at the `Group` level with the `blendMode` prop to composite child elements | paint/overview | +| 13 | Use `ClipRect` / `ClipPath` as children of a `Group` or drawing element to mask content | canvas/overview | +| 14 | Use `SweepGradient` / `TwoPointConicalGradient` for angular and conical fills beyond linear/radial | shaders/gradients | +| 15 | Apply effects to a group composite with the `layer` prop (`` + image filters), not per-child filters alone | group | +| 16 | Use `Skia.ParagraphBuilder` + `` for multi-style text layouts; call `layout(width)` before rendering | text/paragraph | +| 17 | Isolate imperative canvas transforms with `canvas.save()` / `canvas.restore()` inside a recorded `Picture` | shapes/pictures | +| 18 | Load Lottie JSON with `Skia.Skottie.Make(JSON.stringify(...))`; drive `` playback with `useClock` and a Reanimated derived frame | skottie | + +## Eval traceability + +| Eval | Primary best-practice rule(s) | +| --------------------------------- | ----------------------------- | +| 01 – canvas-fill-background | 1, 5 | +| 02 – shape-primitives | 1 | +| 03 – path-drawing | 6 | +| 04 – paint-stroke-fill | 7 | +| 05 – linear-gradient | 1, 2, 3 | +| 06 – radial-gradient | 1 | +| 07 – image-display | 1 | +| 08 – text-rendering | 11 | +| 09 – blur-filter | 8 | +| 10 – color-matrix-filter | 8 | +| 11 – reanimated-basic-animation | 2 | +| 12 – derived-value-animation | 2 | +| 13 – animated-color-interpolation | 3 | +| 14 – gesture-pan | 4 | +| 15 – transforms | 2 | +| 16 – clip-rect-and-path | 13 | +| 17 – blend-mode | 12 | +| 18 – svg-path-rendering | 6 | +| 19 – runtime-effect-shader | 9 | +| 20 – canvas-snapshot | 10 | +| 21 – sweep-gradient | 14 | +| 22 – group-layer-effect | 15 | +| 23 – paragraph-styled-text | 16 | +| 24 – picture-save-restore | 17 | +| 25 – lottie-playback | 1, 2, 18 | + +## API coverage notes + +The pack now covers three built-in gradient shaders (linear, radial, sweep). Major APIs still intentionally omitted — either asset-heavy, niche, or overlapping existing evals: + +| API area | Examples | Why omitted | +| ------------------------- | ------------------------------------------ | -------------------------------------------------------------------------- | +| Conical gradient | `TwoPointConicalGradient` | Overlaps sweep/radial family; add only if conical spotlight effects matter | +| Mask compositing | `` with `alpha` or `luminance` mode | Distinct from clip; good candidate if compositing pack expands | +| Neumorphism shadows | `Box` + `BoxShadow`, `Shadow`/`DropShadow` | UI-specific; overlaps blur/filter evals conceptually | +| Canvas sizing (UI thread) | `onSize` shared value prop | Partially covered by `useCanvasSize` in eval 01 | +| Per-element gestures | `Animated.View` overlay tracking | Documented in rule 4; eval 14 only tests canvas-level pan | +| Nested image shaders | `ImageShader` inside custom `Shader` | Advanced; needs bundled image asset | +| Fitbox / Group clip props | `FitBox`, `Group clip` / `invertClip` | Overlap path/SVG evals; `FitBox` is a strong future add | +| Path / mask filters | `DashPathEffect`, `BlurMask`, morphology | Lower-level paint modifiers; narrow use cases | + +## Common issue clusters + +| Tag | Examples | +| --------------- | -------------------------------------------------------------------------------------------------------------------------------- | +| API misuse | Using `interpolateColor` from Reanimated instead of `interpolateColors` from Skia; using `createAnimatedComponent` unnecessarily | +| Setup | Forgetting `` wrapper; rendering RN `` instead of Skia `` | +| Performance | Per-render path construction instead of memoization; creating `SkPaint` objects inside render | +| Platform parity | `y` origin for `Text` is baseline not top-left (differs from RN Text) | +| Edge case | Not handling `useImage` null state before image is loaded | diff --git a/runner/config.ts b/runner/config.ts index ae87103..1d87f92 100644 --- a/runner/config.ts +++ b/runner/config.ts @@ -1,5 +1,7 @@ import { parseArgs as parseArgv } from 'node:util' +const OPENCODE_DEFAULT_PORT = '4096' + function parsePositiveInteger(rawValue: string, flagName: string) { const parsedValue = Number.parseInt(rawValue, 10) if (!Number.isInteger(parsedValue) || parsedValue <= 0) { @@ -9,8 +11,8 @@ function parsePositiveInteger(rawValue: string, flagName: string) { return parsedValue } -function parsePort(rawValue: string | undefined) { - return rawValue ? parsePositiveInteger(rawValue, '--port') : undefined +function parsePort(rawValue: string) { + return parsePositiveInteger(rawValue, '--port') } /* @@ -26,7 +28,7 @@ export function parseRunCliArgs(argv: string[] = Bun.argv.slice(2)) { 'model': { type: 'string' }, 'pattern': { type: 'string', default: 'evals/**/*' }, 'timeout': { type: 'string', default: '120000' }, - 'port': { type: 'string' }, + 'port': { type: 'string', default: OPENCODE_DEFAULT_PORT }, 'output': { type: 'string' }, }, strict: true, @@ -65,7 +67,7 @@ export function parseJudgeCliArgs(argv: string[] = Bun.argv.slice(2)) { 'rerun-requirement-id': { type: 'string' }, 'rerun-requirements-file': { type: 'string' }, 'timeout': { type: 'string', default: '120000' }, - 'port': { type: 'string' }, + 'port': { type: 'string', default: OPENCODE_DEFAULT_PORT }, 'input': { type: 'string' }, 'output': { type: 'string' }, }, diff --git a/runner/evaluators/llm/judge-client.ts b/runner/evaluators/llm/judge-client.ts index e504f40..102df4f 100644 --- a/runner/evaluators/llm/judge-client.ts +++ b/runner/evaluators/llm/judge-client.ts @@ -1,4 +1,9 @@ -import { Output, generateText } from 'ai' +import { + Output, + extractJsonMiddleware, + generateText, + wrapLanguageModel, +} from 'ai' import { createOpencode } from 'ai-sdk-provider-opencode-sdk' import { z } from 'zod' import { @@ -49,7 +54,7 @@ type RunJudgeCallOptions = { prompt: string model: string timeout: number - port?: number + port: number directory?: string } @@ -110,14 +115,20 @@ function parseJudgeOutputFromText(rawText: string) { export async function runJudgeCall( options: RunJudgeCallOptions ): Promise { - await ensureOpencodeServerStarted({ timeout: options.timeout, port: options.port }) + await ensureOpencodeServerStarted({ + timeout: options.timeout, + port: options.port, + }) const provider = createOpencode({ autoStartServer: false, port: options.port, }) - const judgeModel = provider(options.model, { createNewSession: true }) + const judgeModel = wrapLanguageModel({ + model: provider(options.model, { createNewSession: true }), + middleware: extractJsonMiddleware(), + }) try { const response = await generateText({ diff --git a/runner/evaluators/llm/run.ts b/runner/evaluators/llm/run.ts index 4e105a0..9ee6b8a 100644 --- a/runner/evaluators/llm/run.ts +++ b/runner/evaluators/llm/run.ts @@ -7,7 +7,7 @@ import type { LoadedFile } from 'runner/utils/fs' type LlmJudgeStageOptions = { model: string timeout: number - port?: number + port: number directory?: string requirementIds?: string[] } diff --git a/runner/solver/index.ts b/runner/solver/index.ts index 047d51c..28c448d 100644 --- a/runner/solver/index.ts +++ b/runner/solver/index.ts @@ -1,4 +1,9 @@ -import { Output, generateText } from 'ai' +import { + Output, + extractJsonMiddleware, + generateText, + wrapLanguageModel, +} from 'ai' import { createOpencode } from 'ai-sdk-provider-opencode-sdk' import { mkdir, writeFile } from 'node:fs/promises' import path from 'node:path' @@ -28,12 +33,14 @@ const JSON_FALLBACK_SYSTEM_PROMPT = ` const solverOutputSchema = z.object({ summary: z.string().describe('Short summary of performed work'), - files: z.array( - z.object({ - path: z.string(), - content: z.string(), - }) - ).min(1), + files: z + .array( + z.object({ + path: z.string(), + content: z.string(), + }) + ) + .min(1), }) export type SolverResult = { @@ -148,12 +155,12 @@ function parseSolverOutputFromText(rawText: string) { Runs an OpenCode-backed solver and materializes generated files for judges. */ export async function runSolver(params: { - prompt: string, - files: LoadedFile[], - workingDirectory: string, + prompt: string + files: LoadedFile[] + workingDirectory: string model: string timeout: number - port?: number + port: number }) { await ensureOpencodeServerStarted(params) @@ -163,9 +170,13 @@ export async function runSolver(params: { }) const prompt = buildSolverPrompt(params.prompt, params.files) - const model = provider(params.model, { - createNewSession: true, - cwd: params.workingDirectory, + + const model = wrapLanguageModel({ + model: provider(params.model, { + createNewSession: true, + cwd: params.workingDirectory, + }), + middleware: extractJsonMiddleware(), }) try { diff --git a/runner/solver/pipeline.ts b/runner/solver/pipeline.ts index 6f64b6d..898a142 100644 --- a/runner/solver/pipeline.ts +++ b/runner/solver/pipeline.ts @@ -4,7 +4,7 @@ import { type LoadedFile } from 'runner/utils/fs' type SolverStageOptions = { solverModel: string timeout: number - port?: number + port: number } /* @@ -28,9 +28,6 @@ export async function runSolverStage( return { summary: result.summary, opencodeSession: result.opencodeSession, - files: await materializeFiles( - workingDir, - result.files - ), + files: await materializeFiles(workingDir, result.files), } } diff --git a/runner/utils/opencode-session.ts b/runner/utils/opencode-session.ts index 4872c17..9007954 100644 --- a/runner/utils/opencode-session.ts +++ b/runner/utils/opencode-session.ts @@ -83,7 +83,10 @@ function extractMessageContent(parts: Part[]) { } if (part.type === 'tool') { - if (part.state.status === 'completed' && part.state.output.trim().length > 0) { + if ( + part.state.status === 'completed' && + part.state.output.trim().length > 0 + ) { segments.push(part.state.output) } continue @@ -181,12 +184,14 @@ function uniqueTouchedFiles( } function summarizeChanges( - sessionSummary: { - additions: number - deletions: number - files: number - diffs?: FileDiff[] - } | undefined, + sessionSummary: + | { + additions: number + deletions: number + files: number + diffs?: FileDiff[] + } + | undefined, sessionDiffs: FileDiff[] ) { const sessionDiffAdditions = sessionDiffs.reduce( @@ -210,7 +215,7 @@ function summarizeChanges( export async function collectOpencodeSessionSnapshot(params: { sessionId?: string - port?: number + port: number directory?: string }): Promise { if (!params.sessionId) { @@ -228,21 +233,21 @@ export async function collectOpencodeSessionSnapshot(params: { try { const [sessionResponse, messagesResponse, sessionDiffsResponse] = await Promise.all([ - client.session.get({ - path: { id: params.sessionId }, - query: { directory: params.directory }, - throwOnError: true, - }), - client.session.messages({ - path: { id: params.sessionId }, - query: { directory: params.directory, limit: 500 }, - throwOnError: true, - }), - client.session.diff({ - path: { id: params.sessionId }, - query: { directory: params.directory }, - throwOnError: true, - }), + client.session.get({ + path: { id: params.sessionId }, + query: { directory: params.directory }, + throwOnError: true, + }), + client.session.messages({ + path: { id: params.sessionId }, + query: { directory: params.directory, limit: 500 }, + throwOnError: true, + }), + client.session.diff({ + path: { id: params.sessionId }, + query: { directory: params.directory }, + throwOnError: true, + }), ]) session = sessionResponse.data @@ -286,9 +291,7 @@ export async function collectOpencodeSessionSnapshot(params: { const messageSnapshots = typedMessages .map((message) => summarizeMessage(message)) - .sort((first, second) => - first.createdAt.localeCompare(second.createdAt) - ) + .sort((first, second) => first.createdAt.localeCompare(second.createdAt)) const userTurns = messageSnapshots.filter( (message) => message.role === 'user' diff --git a/runner/utils/opencode.ts b/runner/utils/opencode.ts index 4d6a225..015efbe 100644 --- a/runner/utils/opencode.ts +++ b/runner/utils/opencode.ts @@ -1,17 +1,17 @@ -import { createConnection } from 'node:net' - import { createOpencodeServer } from '@opencode-ai/sdk/v2/server' let serverPromise: Promise | undefined -function isPortInUse(port: number, host = '127.0.0.1'): Promise { - return new Promise((resolve) => { - const socket = createConnection(port, host, () => { - socket.destroy() - resolve(true) +async function isPortInUse(port: number): Promise { + const base = `http://127.0.0.1:${port}` + try { + const res = await fetch(`${base}/global/health`, { + signal: AbortSignal.timeout(1500), }) - socket.on('error', () => resolve(false)) - }) + return res.ok + } catch { + return false + } } /* @@ -22,17 +22,16 @@ export async function ensureOpencodeServerStarted({ port, timeout = 120000, }: { - port?: number + port: number timeout?: number }) { - const portToUse = port ?? 4096 - if (await isPortInUse(portToUse)) { + if (await isPortInUse(port)) { return } if (!serverPromise) { serverPromise = (async () => { await createOpencodeServer({ - port: portToUse, + port: port, timeout, }) })()