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
124 changes: 124 additions & 0 deletions docs/demos/demos/groups/export.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import type { ComponentType } from 'react';
import { useRef, useState } from 'react';
import {
Image as RNImage,
Pressable,
StyleSheet,
Text as RNText,
View,
useWindowDimensions,
} from 'react-native';
import {
Stage,
Layer,
Rect,
Circle,
Text,
useFont,
type StageHandle,
} from 'react-native-canvas-kit';
import { FONT_URL } from '../../src/scene';

function ExportToImage() {
const { width, height } = useWindowDimensions();
const font = useFont(FONT_URL, 30);
const stageRef = useRef<StageHandle>(null);
const [uri, setUri] = useState<string | null>(null);

const handleExport = async () => {
const dataUrl = await stageRef.current?.toDataURL({ mimeType: 'image/png' });
if (dataUrl) setUri(dataUrl);
};

const centerX = width / 2;
const centerY = height / 2;

return (
<View style={styles.root}>
<Stage
ref={stageRef}
width={width}
height={height}
style={styles.stage}
>
<Layer width={width} height={height} gestureEnabled>
<Rect
x={centerX - 130}
y={centerY - 70}
width={150}
height={110}
cornerRadius={16}
fill="#8a2be2"
draggable
/>
<Circle x={centerX + 70} y={centerY} radius={54} fill="#22d3ee" draggable />
{font && (
<Text
text="Drag, then Export"
x={centerX - 120}
y={centerY + 90}
font={font}
fill="#1b0030"
/>
)}
</Layer>
</Stage>

<Pressable style={styles.exportButton} onPress={handleExport} hitSlop={8}>
<RNText style={styles.exportButtonText}>Export PNG</RNText>
</Pressable>

{uri && (
<View style={styles.preview} pointerEvents="none">
<RNText style={styles.previewLabel}>Exported image</RNText>
<RNImage source={{ uri }} style={styles.previewImage} resizeMode="cover" />
</View>
)}
</View>
);
}

const styles = StyleSheet.create({
root: { flex: 1 },
stage: { backgroundColor: '#faf7ff' },
exportButton: {
position: 'absolute',
top: 16,
right: 16,
backgroundColor: '#8a2be2',
paddingHorizontal: 18,
paddingVertical: 10,
borderRadius: 22,
},
exportButtonText: {
color: '#ffffff',
fontSize: 14,
fontWeight: '700',
},
preview: {
position: 'absolute',
bottom: 16,
left: 16,
padding: 8,
borderRadius: 12,
backgroundColor: '#ffffff',
borderWidth: 1,
borderColor: '#e5d5ff',
},
previewLabel: {
fontSize: 11,
fontWeight: '600',
color: '#6D28D9',
marginBottom: 6,
},
previewImage: {
width: 120,
height: 90,
borderRadius: 6,
backgroundColor: '#faf7ff',
},
});

export const exportDemos: Record<string, ComponentType> = {
'export-overview-1': ExportToImage,
};
2 changes: 2 additions & 0 deletions docs/demos/demos/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { shapesExtraDemos } from './groups/shapesExtra';
import { stylingDemos } from './groups/styling';
import { interactivityDemos } from './groups/interactivity';
import { brushesPortalDemos } from './groups/brushesPortal';
import { exportDemos } from './groups/export';

export const DEMOS: Record<string, ComponentType> = {
...gettingStartedDemos,
Expand All @@ -15,4 +16,5 @@ export const DEMOS: Record<string, ComponentType> = {
...stylingDemos,
...interactivityDemos,
...brushesPortalDemos,
...exportDemos,
};
7 changes: 7 additions & 0 deletions docs/docs/core-concepts/stage.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,13 @@ The stage itself is transparent. Give it a background with `style`:
<Stage width={width} height={height} style={{ backgroundColor: '#a441e1' }} />
```

## Exporting to an image

Attach a `ref` to the stage to snapshot the canvas to an image via
`toDataURL`, `toBase64`, or `makeImageSnapshot`. See
[Export to Image](../export/overview.md) for the full API and an interactive
demo.

## Disabling interaction

- `listening={false}` turns the stage into a static, non-interactive drawing.
Expand Down
8 changes: 8 additions & 0 deletions docs/docs/export/_category_.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"label": "Export",
"position": 9,
"link": {
"type": "doc",
"id": "export/overview"
}
}
93 changes: 93 additions & 0 deletions docs/docs/export/overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
---
sidebar_position: 1
title: Export to Image
---

import Demo from '@site/src/components/Demo';

# Export to Image

Attach a `ref` to a [`Stage`](../core-concepts/stage.md) to snapshot the canvas
to a Skia image, a base64 string, or a data URL. This is the equivalent of
Konva's
[high-quality export](https://konvajs.org/docs/data_and_serialization/High-Quality-Export.html).

<Demo name="export-overview-1" title="Drag the shapes, then tap Export" />

Drag the shapes around, tap **Export PNG**, and the exported snapshot appears in
the corner: it captures the canvas exactly as drawn.

## Why it is high quality by default

On the web a `<canvas>` is backed at 1x, which is why Konva needs an explicit
`pixelRatio` to export at retina quality. On React Native the Skia surface is
already backed at the device pixel ratio, so a snapshot is high-DPI by default:
a 300x300 stage on a 3x device exports a 900x900 image.

## Usage

```tsx
import { useRef } from 'react';
import { Stage, Layer, Rect, type StageHandle } from 'react-native-canvas-kit';

function Scene() {
const stageRef = useRef<StageHandle>(null);

const handleExport = async () => {
const dataUrl = await stageRef.current?.toDataURL();
// dataUrl: "data:image/png;base64,..." - render in an <Image> or save it.

const jpegBase64 = await stageRef.current?.toBase64({
mimeType: 'image/jpeg',
quality: 0.8,
});

const image = await stageRef.current?.makeImageSnapshot();
// image: a Skia SkImage for further Skia processing.
};

return (
<Stage ref={stageRef} width={300} height={300}>
<Layer>
<Rect x={20} y={20} width={100} height={100} fill="#8a2be2" />
</Layer>
</Stage>
);
}
```

## Methods

| Method | Returns | Description |
| -------------------- | ----------------------------- | -------------------------------------------------- |
| `makeImageSnapshot` | `Promise<SkImage \| null>` | Raw Skia image for further processing. |
| `toBase64` | `Promise<string \| null>` | Encoded image as a bare base64 string. |
| `toDataURL` | `Promise<string \| null>` | Encoded image as a `data:<mime>;base64,...` URL. |

## Options

All three methods accept an optional `StageToImageOptions`:

| Option | Type | Default | Description |
| ---------- | ----------------------------------------------- | ------------- | ------------------------------------------------------------ |
| `mimeType` | `'image/png' \| 'image/jpeg' \| 'image/webp'` | `'image/png'` | Output format (ignored by `makeImageSnapshot`). |
| `quality` | `number` (0-1) | `1` | Compression quality for JPEG/WebP. |
| `x` | `number` | `0` | Crop origin x, in points. |
| `y` | `number` | `0` | Crop origin y, in points. |
| `width` | `number` | Full width | Crop width, in points. Pass with `height` to crop a region. |
| `height` | `number` | Full height | Crop height, in points. Pass with `width` to crop a region. |

## Cropping a region

Pass `x`, `y`, `width`, and `height` (in points) to export just part of the
stage. This is how a square crop or a fixed-aspect thumbnail is produced from a
larger canvas:

```tsx
const thumbnail = await stageRef.current?.toDataURL({
x: 40,
y: 40,
width: 200,
height: 200,
});
```
Loading
Loading