Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@

# react-native-canvas-kit

<p align="center">
<video src="https://github.com/adithyavis/react-native-canvas-kit/raw/main/assets/landscape.mp4" width="100%" controls muted autoplay loop></video>
</p>

A **batteries-included 2D canvas kit for React Native**, built on top of
[React Native Skia](https://shopify.github.io/react-native-skia/). Canvas Kit
layers a **scene graph** over Skia and ships with pre-built shapes, gestures and
Expand Down
Binary file added assets/landscape.mp4
Binary file not shown.
59 changes: 59 additions & 0 deletions docs/docs/getting-started/web.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
---
sidebar_position: 3
title: Web
---

# Web

Canvas Kit runs in the browser through [React Native Skia](https://shopify.github.io/react-native-skia/docs/getting-started/web),
which ships Skia as a WebAssembly (WASM) build called **CanvasKit**.

## Install the web dependency

CanvasKit is delivered by the `canvaskit-wasm` package. Add it alongside the peer
dependencies from [Installation](./installation.md), then run Skia's web setup
script once:

```bash
npx expo install @shopify/react-native-skia
npx setup-skia-web
```

Re-run `setup-skia-web` after every upgrade of `@shopify/react-native-skia` so the
copied WASM binary matches your installed version.

For the full setup (bundler configuration, deferred registration, and other
variants), follow the
[React Native Skia web guide](https://shopify.github.io/react-native-skia/docs/getting-started/web/).
The rest of this page covers the load-order details that matter specifically for
Canvas Kit.

## Load order

Web setup, including loading CanvasKit before your app runs, is handled entirely
by React Native Skia. Follow their
[web support guide](https://shopify.github.io/react-native-skia/docs/getting-started/web/)
for `LoadSkiaWeb` / `WithSkiaWeb` and the matching bundler configuration.

One Canvas Kit-specific caveat: its shape components touch Skia at import time,
while the module tree is evaluated. You must therefore use a load-first pattern,
either a `.web` entry that loads CanvasKit before requiring your app, or Skia's
`WithSkiaWeb` component that lazily imports the drawing code. Deferring the load
to a `useEffect` is too late and will throw.

## Caveats

- **WebGL contexts are limited.** Browsers cap live WebGL contexts (around 16 per
page), and every mounted `Stage` holds one. Keep the number of simultaneous
stages small on web.
- **A few Skia features are unavailable on web**, including
`PathEffectFactory.MakeSum` / `MakeCompose`, `PathFactory.MakeFromText`, and
`ShaderFilter`. Canvas Kit does not rely on these for its core shapes, but
custom brushes or effects that reach into Skia directly may hit them.
- **Reanimated and Gesture Handler** power dragging and gestures; make sure your
web bundler is configured for Reanimated as described in its
[web docs](https://docs.swmansion.com/react-native-reanimated/docs/guides/web-support/).

For the full, authoritative reference (webpack config, deferred registration
variants, and more), see the
[React Native Skia web guide](https://shopify.github.io/react-native-skia/docs/getting-started/web).
5 changes: 4 additions & 1 deletion docs/docs/intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ title: Introduction

# React Native Canvas Kit

<video src="/react-native-canvas-kit/landscape.mp4" autoPlay loop muted playsInline style={{ width: '100%', aspectRatio: '16 / 9', borderRadius: 8 }} />

**React Native Canvas Kit** is a batteries-included 2D canvas kit built on
top of [React Native Skia](https://shopify.github.io/react-native-skia/). Canvas Kit layers a
**scene graph** on top of Skia and comes with pre-built shapes, support for gestures and
interactivity, transformers, and brushes, so that you don't have to compose a canvas experience
from scratch.

React Native Canvas Kit is heavily inspired by [Konva](https://https://konvajs.org/).
React Native Canvas Kit is heavily inspired by [Konva](https://konvajs.org/); see
[Thanks to Konva](./thanks-to-konva.md) for what the two share.

## The scene graph

Expand Down
43 changes: 43 additions & 0 deletions docs/docs/thanks-to-konva.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
sidebar_position: 9
title: Thanks to Konva
---

# Thanks to Konva

I built React Native Canvas Kit because I kept wishing [Konva](https://konvajs.org/)
existed for React Native. Konva is a fantastic 2D scene graph for the web canvas,
and years of using it shaped how I think a canvas library should feel.

So before anything else, thank you to [Anton Lavrenov](https://github.com/lavrton) and the
Konva community. This library would not exist without that groundwork.

## What I kept similar

I deliberately kept Canvas Kit close to Konva so that anyone who already knows
Konva feels at home right away.

- **The scene graph.** The hierarchy is the same idea:
`Stage → Layer → Group → Shape`. Nodes nest, transforms cascade down the tree,
and each node maps onto a render node underneath.
- **Prop names.** Shapes take the props you would expect from Konva:
`x`, `y`, `width`, `height`, `radius`, `fill`, `stroke`, `strokeWidth`,
`scaleX`, `scaleY`, `rotation`, and so on. If you have written Konva, most of
your muscle memory carries over.
- **Interactivity.** Events bubble through ancestors with hierarchy-aware hit
testing, `draggable` makes a node follow the pointer, and a `Transformer` you
point at a node draws resize and rotate handles. These mirror Konva's model
closely, including the `cancelBubble` escape hatch on events.

## What is different

The concepts are shared, but the engine is not. Canvas Kit renders with
[React Native Skia](https://shopify.github.io/react-native-skia/) rather than the
web `<canvas>`, and it drives dragging and multi-touch gestures on the UI thread
with [Reanimated](https://docs.swmansion.com/react-native-reanimated/) and
[Gesture Handler](https://docs.swmansion.com/react-native-gesture-handler/). That
lets interactions stay smooth on a phone, but it also means some things diverge
from Konva where React Native and Skia call for it.

If you are coming from Konva: welcome. I hope this feels familiar. And thank you
again to the Konva project for the inspiration.
Binary file added docs/static/landscape.mp4
Binary file not shown.
65 changes: 65 additions & 0 deletions docs/static/llms.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# React Native Canvas Kit

> React Native Canvas Kit is a batteries-included 2D canvas library for React Native. It layers a Konva-style scene graph (Stage, Layer, Group, Shape) on top of React Native Skia and ships pre-built shapes, hierarchy-aware events, draggable nodes, multi-touch gestures, an interactive Transformer, and a set of brushes. Gestures and dragging run on the UI thread via Reanimated and Gesture Handler. It requires React Native's New Architecture; the 0.x line targets Reanimated 3 / legacy architecture.

## Keywords

react-native-canvas-kit, react-native-canvas, canvas for react native, react-native-konva, konva for react native, konva react native, expo-canvas, canvas for expo, react-native-skia canvas, skia scene graph, 2D canvas library react native, the only canvas you would need on react native, react native drawing library, react native shapes, react native transformer, react native brushes, react native gestures canvas, interactive canvas react native, HTML5 canvas alternative react native.

## Getting Started

- [Introduction](https://adithyavis.github.io/react-native-canvas-kit/intro): What Canvas Kit is, the scene graph model, and what the library provides.
- [Installation](https://adithyavis.github.io/react-native-canvas-kit/getting-started/installation): Install the library and its Skia, Reanimated, and Gesture Handler peer dependencies, and configure Babel.
- [Quick Start](https://adithyavis.github.io/react-native-canvas-kit/getting-started/quick-start): Build a first interactive canvas with shapes, dragging, and a transformer.
- [Web](https://adithyavis.github.io/react-native-canvas-kit/getting-started/web): Run Canvas Kit in the browser via Skia's CanvasKit WASM, including the load-order rule and a web-only entry point.

## Core Concepts

- [Stage](https://adithyavis.github.io/react-native-canvas-kit/core-concepts/stage): The root node that owns the native Skia drawing surface.
- [Layer](https://adithyavis.github.io/react-native-canvas-kit/core-concepts/layer): A logical grouping with its own transform inside a Stage.
- [Group](https://adithyavis.github.io/react-native-canvas-kit/core-concepts/group): A transformable container for shapes and nested groups.
- [Nodes and transforms](https://adithyavis.github.io/react-native-canvas-kit/core-concepts/nodes-and-transforms): How position, scale, and rotation cascade down the node tree.

## Shapes

- [Overview](https://adithyavis.github.io/react-native-canvas-kit/shapes/overview): The shared transform and styling props common to every shape.
- [Rect](https://adithyavis.github.io/react-native-canvas-kit/shapes/rect): Rectangles with optional corner radius.
- [Circle](https://adithyavis.github.io/react-native-canvas-kit/shapes/circle): Circles by center and radius.
- [Ellipse](https://adithyavis.github.io/react-native-canvas-kit/shapes/ellipse): Ellipses with independent x and y radii.
- [Line](https://adithyavis.github.io/react-native-canvas-kit/shapes/line): Polylines and open or closed point paths.
- [Regular Polygon](https://adithyavis.github.io/react-native-canvas-kit/shapes/regular-polygon): Equilateral polygons by side count and radius.
- [Star](https://adithyavis.github.io/react-native-canvas-kit/shapes/star): Stars with inner and outer radii.
- [Text](https://adithyavis.github.io/react-native-canvas-kit/shapes/text): Text drawing and styling.
- [Image](https://adithyavis.github.io/react-native-canvas-kit/shapes/image): Rendering images onto the canvas.

## Styling

- [Fill and stroke](https://adithyavis.github.io/react-native-canvas-kit/styling/fill-and-stroke): Fill colors, stroke color, and stroke width.
- [Gradients](https://adithyavis.github.io/react-native-canvas-kit/styling/gradients): Linear and radial gradient fills.
- [Shadows and blend](https://adithyavis.github.io/react-native-canvas-kit/styling/shadows-and-blend): Drop shadows and blend modes.

## Interactivity

- [Events](https://adithyavis.github.io/react-native-canvas-kit/interactivity/events): Tap and press events with hierarchy-aware hit testing and ancestor bubbling.
- [Drag and drop](https://adithyavis.github.io/react-native-canvas-kit/interactivity/drag-and-drop): Making nodes draggable and handling drag lifecycle events.
- [Gestures](https://adithyavis.github.io/react-native-canvas-kit/interactivity/gestures): UI-thread pinch-to-scale and rotate multi-touch gestures.
- [Transformer](https://adithyavis.github.io/react-native-canvas-kit/interactivity/transformer): An attachable selection box with resize and rotate handles.
- [Bounds and snapping](https://adithyavis.github.io/react-native-canvas-kit/interactivity/bounds-and-snapping): Node bounds and snapping during transforms.

## Brushes

- [Overview](https://adithyavis.github.io/react-native-canvas-kit/brushes/overview): The BrushLayer and UI-thread stroke capture.
- [Brush types](https://adithyavis.github.io/react-native-canvas-kit/brushes/brush-types): Ready-made brushes (pen, pencil, marker, highlighter, tape, eraser).
- [Custom brushes](https://adithyavis.github.io/react-native-canvas-kit/brushes/custom-brushes): Building your own brushes.

## Portal

- [Overview](https://adithyavis.github.io/react-native-canvas-kit/portal/overview): Rendering nodes into a different part of the scene graph.

## More

- [Thanks to Konva](https://adithyavis.github.io/react-native-canvas-kit/thanks-to-konva): The Konva inspiration behind the API, and what is kept similar or different.

## Source

- [GitHub repository](https://github.com/adithyavis/react-native-canvas-kit): Source, issues, and releases for React Native Canvas Kit.
75 changes: 73 additions & 2 deletions example/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,51 @@
ReanimatedLogLevel,
} from 'react-native-reanimated';
import { Drawer } from 'expo-router/drawer';
import { StyleSheet } from 'react-native';
import {
DrawerContentScrollView,
DrawerItem,
type DrawerContentComponentProps,
} from '@react-navigation/drawer';
import { StyleSheet, View } from 'react-native';

configureReanimatedLogger({ level: ReanimatedLogLevel.warn, strict: false });

const DIVIDER_AFTER = 'portal';

function DrawerContent(props: DrawerContentComponentProps) {
const { state, descriptors, navigation } = props;
return (
<DrawerContentScrollView {...props}>
{state.routes.map((route, index) => {
const options = descriptors[route.key]?.options ?? {};
const label =
typeof options.drawerLabel === 'string'
? options.drawerLabel
: (options.title ?? route.name);
return (
<View key={route.key}>
<DrawerItem
label={label}
focused={index === state.index}
onPress={() => navigation.navigate(route.name)}
/>
{route.name === DIVIDER_AFTER ? (
<View style={styles.divider} />
) : null}
</View>
);
})}
</DrawerContentScrollView>
);
}

export default function RootLayout() {
return (
<GestureHandlerRootView style={styles.flex1}>
<Drawer screenOptions={{ headerShown: false, swipeEnabled: false }}>
<Drawer
drawerContent={(props) => <DrawerContent {...props} />}

Check warning on line 49 in example/app/_layout.tsx

View workflow job for this annotation

GitHub Actions / lint

Do not define components during render. React will see a new component type on every render and destroy the entire subtree’s DOM nodes and state (https://reactjs.org/docs/reconciliation.html#elements-of-different-types). Instead, move this component definition out of the parent component “RootLayout” and pass data as props. If you want to allow component creation in props, set allowAsProps option to true
screenOptions={{ headerShown: false, swipeEnabled: false }}
>
<Drawer.Screen
name="index"
options={{ drawerLabel: 'Shapes', title: 'Shapes' }}
Expand All @@ -20,11 +57,45 @@
name="brushes"
options={{ drawerLabel: 'Brushes', title: 'Brushes' }}
/>
<Drawer.Screen
name="portal"
options={{ drawerLabel: 'Portal', title: 'Portal' }}
/>

<Drawer.Screen
name="canva"
options={{ drawerLabel: 'Canva', title: 'Canva' }}
/>
<Drawer.Screen
name="instagram-crop"
options={{ drawerLabel: 'Instagram Crop', title: 'Instagram Crop' }}
/>
<Drawer.Screen
name="docusign"
options={{ drawerLabel: 'DocuSign', title: 'DocuSign' }}
/>
<Drawer.Screen
name="gpay-scratch-card"
options={{
drawerLabel: 'GPay Scratch Card',
title: 'GPay Scratch Card',
}}
/>
<Drawer.Screen
name="snapseed"
options={{ drawerLabel: 'Snapseed', title: 'Snapseed' }}
/>
</Drawer>
</GestureHandlerRootView>
);
}

const styles = StyleSheet.create({
flex1: { flex: 1 },
divider: {
height: 1,
backgroundColor: '#00000022',
marginVertical: 8,
marginHorizontal: 16,
},
});
57 changes: 54 additions & 3 deletions example/app/brushes.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,74 @@
import { useRef, useState } from 'react';
import { useMemo, useRef, useState } from 'react';
import {
Pressable,
StyleSheet,
Image as RNImage,
View,
useWindowDimensions,
} from 'react-native';
import {
Canvas,
Fill,
Points,
vec,
type SkPoint,
} from '@shopify/react-native-skia';
import { Stage, BrushLayer, BRUSH_PATHS } from 'react-native-canvas-kit';
import { DrawerButton } from '../src/DrawerButton';
import { useTouchTracker, TouchRings } from '../src/TouchOverlay';
import { TOOLS, type DrawnStroke, type Tool } from '../src/constants';

const DOT_SPACING = 24;
const DOT_SIZE = 2.5;
const BG_COLOR = '#F7F6F3';
const DOT_COLOR = 'rgba(0,0,0,0.13)';

function DotGrid({ width, height }: { width: number; height: number }) {
const dots = useMemo<SkPoint[]>(() => {
const points: SkPoint[] = [];
for (let x = DOT_SPACING; x < width; x += DOT_SPACING) {
for (let y = DOT_SPACING; y < height; y += DOT_SPACING) {
points.push(vec(x, y));
}
}
return points;
}, [width, height]);

return (
<View style={StyleSheet.absoluteFill} pointerEvents="none">
<Canvas style={styles.flex1}>
<Fill color={BG_COLOR} />
<Points
points={dots}
mode="points"
color={DOT_COLOR}
style="stroke"
strokeWidth={DOT_SIZE}
strokeCap="round"
/>
</Canvas>
</View>
);
}

export default function BrushesScreen() {
const { width, height } = useWindowDimensions();
const [tool, setTool] = useState<Tool>(TOOLS[0].tool);
const [strokes, setStrokes] = useState<DrawnStroke[]>([]);
const strokeCounter = useRef(0);
const { gesture: touchGesture, touches } = useTouchTracker();

const toggleTool = (next: NonNullable<Tool>) => setTool(next);

return (
<View style={styles.root}>
<Stage width={width} height={height} style={styles.stage}>
<DotGrid width={width} height={height} />
<Stage
width={width}
height={height}
style={styles.stage}
simultaneousGesture={touchGesture}
>
<BrushLayer
tool={tool}
onStrokeEnd={({ points, tool: usedTool }) =>
Expand All @@ -41,6 +89,8 @@ export default function BrushesScreen() {
</BrushLayer>
</Stage>

<TouchRings touches={touches} />

<View style={styles.toolbar} pointerEvents="box-none">
{TOOLS.map(({ tool: t, icon }) => (
<Pressable
Expand All @@ -64,7 +114,8 @@ export default function BrushesScreen() {

const styles = StyleSheet.create({
root: { flex: 1 },
stage: { backgroundColor: '#f1d1ff' },
flex1: { flex: 1 },
stage: { backgroundColor: 'transparent' },
toolbar: {
position: 'absolute',
bottom: 40,
Expand Down
Loading
Loading