From 5f782a49ecf0a8ae46f1a584b779e47e7e3de233 Mon Sep 17 00:00:00 2001 From: adithyavis Date: Fri, 24 Jul 2026 16:36:22 +0530 Subject: [PATCH 1/5] feat: add Stage image export API --- docs/docs/core-concepts/stage.md | 62 +++++++++++++++++++ src/components/Stage.tsx | 101 +++++++++++++++++++++++++++---- src/index.tsx | 6 +- 3 files changed, 155 insertions(+), 14 deletions(-) diff --git a/docs/docs/core-concepts/stage.md b/docs/docs/core-concepts/stage.md index 158251c..aa8856c 100644 --- a/docs/docs/core-concepts/stage.md +++ b/docs/docs/core-concepts/stage.md @@ -60,6 +60,68 @@ The stage itself is transparent. Give it a background with `style`: ``` +## Exporting to an image + +Attach a `ref` to the stage 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). + +On the web a `` 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. + +```tsx +import { useRef } from 'react'; +import { Stage, Layer, Rect, type StageHandle } from 'react-native-canvas-kit'; + +function Scene() { + const stageRef = useRef(null); + + const handleExport = async () => { + const dataUrl = await stageRef.current?.toDataURL(); + // dataUrl: "data:image/png;base64,..." - render in an 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 ( + + + + + + ); +} +``` + +### Methods + +| Method | Returns | Description | +| -------------------- | ----------------------------- | -------------------------------------------------- | +| `makeImageSnapshot` | `Promise` | Raw Skia image for further processing. | +| `toBase64` | `Promise` | Encoded image as a bare base64 string. | +| `toDataURL` | `Promise` | Encoded image as a `data:;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. | + ## Disabling interaction - `listening={false}` turns the stage into a static, non-interactive drawing. diff --git a/src/components/Stage.tsx b/src/components/Stage.tsx index a75ceea..b223506 100644 --- a/src/components/Stage.tsx +++ b/src/components/Stage.tsx @@ -1,6 +1,8 @@ import { + forwardRef, memo, useCallback, + useImperativeHandle, useLayoutEffect, useMemo, useRef, @@ -8,7 +10,13 @@ import { type ReactNode, } from 'react'; import { StyleSheet, View, type StyleProp, type ViewStyle } from 'react-native'; -import { Canvas } from '@shopify/react-native-skia'; +import { + Canvas, + ImageFormat, + Skia, + useCanvasRef, + type SkImage, +} from '@shopify/react-native-skia'; import { Gesture, GestureDetector, @@ -34,6 +42,30 @@ import { } from './internal/gestureState'; import { useStageGestures } from './internal/useStageGestures'; +export interface StageToImageOptions { + mimeType?: 'image/png' | 'image/jpeg' | 'image/webp'; + quality?: number; + x?: number; + y?: number; + width?: number; + height?: number; +} + +export interface StageHandle { + makeImageSnapshot: (options?: StageToImageOptions) => Promise; + toBase64: (options?: StageToImageOptions) => Promise; + toDataURL: (options?: StageToImageOptions) => Promise; +} + +const MIME_TYPE_TO_IMAGE_FORMAT: Record< + NonNullable, + ImageFormat +> = { + 'image/png': ImageFormat.PNG, + 'image/jpeg': ImageFormat.JPEG, + 'image/webp': ImageFormat.WEBP, +}; + export interface StageProps { width: number; height: number; @@ -50,17 +82,22 @@ export interface StageProps { } export const Stage = memo( - ({ - width, - height, - style, - listening = true, - gestureEnabled: _gestureEnabled = true, - pinchSensitivity, - rotationSensitivity, - simultaneousGesture, - children, - }: StageProps) => { + forwardRef(function Stage( + { + width, + height, + style, + listening = true, + gestureEnabled: _gestureEnabled = true, + pinchSensitivity, + rotationSensitivity, + simultaneousGesture, + children, + }, + ref + ) { + const canvasRef = useCanvasRef(); + const registryRef = useRef(null); if (!registryRef.current) { registryRef.current = new NodeRegistry(); @@ -128,9 +165,46 @@ export const Stage = memo( [registerPortal] ); + useImperativeHandle(ref, () => { + const snapshot = async (options?: StageToImageOptions) => { + const canvasHandle = canvasRef.current; + if (!canvasHandle) return null; + const cropRect = + options?.width != null && options?.height != null + ? Skia.XYWHRect( + options.x ?? 0, + options.y ?? 0, + options.width, + options.height + ) + : undefined; + return canvasHandle.makeImageSnapshotAsync(cropRect); + }; + + const encode = async (options?: StageToImageOptions) => { + const image = await snapshot(options); + if (!image) return null; + const format = + MIME_TYPE_TO_IMAGE_FORMAT[options?.mimeType ?? 'image/png']; + const quality = Math.round((options?.quality ?? 1) * 100); + return image.encodeToBase64(format, quality); + }; + + return { + makeImageSnapshot: snapshot, + toBase64: encode, + toDataURL: async (options?: StageToImageOptions) => { + const base64 = await encode(options); + if (base64 == null) return null; + return `data:${options?.mimeType ?? 'image/png'};base64,${base64}`; + }, + }; + }, [canvasRef]); + const canvas = useMemo( () => ( ), [ + canvasRef, children, gestureEnabled, height, @@ -181,6 +256,6 @@ export const Stage = memo( /> ); - } + }) ); Stage.displayName = 'Stage'; diff --git a/src/index.tsx b/src/index.tsx index 670aec0..2cae4f4 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -2,7 +2,11 @@ import { assertReanimatedVersion } from './core/assertReanimatedVersion'; assertReanimatedVersion(); export { Stage } from './components/Stage'; -export type { StageProps } from './components/Stage'; +export type { + StageProps, + StageHandle, + StageToImageOptions, +} from './components/Stage'; export { Layer } from './components/Layer'; export type { LayerProps } from './components/Layer'; export { Group } from './components/Group'; From 01ebf0c97918cb31f6ffe463cf569ceb0b73358d Mon Sep 17 00:00:00 2001 From: adithyavis Date: Fri, 24 Jul 2026 16:36:24 +0530 Subject: [PATCH 2/5] feat: export in canva example --- example/app/canva.tsx | 163 ++++++++++++++++++++++++++++++++---- example/src/CanvaChrome.tsx | 11 ++- 2 files changed, 151 insertions(+), 23 deletions(-) diff --git a/example/app/canva.tsx b/example/app/canva.tsx index 6b64ca0..0b0b0e7 100644 --- a/example/app/canva.tsx +++ b/example/app/canva.tsx @@ -1,5 +1,19 @@ -import { useMemo, useState, type Dispatch, type SetStateAction } from 'react'; -import { StyleSheet, View } from 'react-native'; +import { + useMemo, + useRef, + useState, + type Dispatch, + type RefObject, + type SetStateAction, +} from 'react'; +import { + Image as RNImage, + Modal, + Pressable, + StyleSheet, + Text as RNText, + View, +} from 'react-native'; import { Asset } from 'expo-asset'; import { Stage, @@ -9,6 +23,7 @@ import { Transformer, useFont, type EventObject, + type StageHandle, type TransformEvent, type TransformResult, } from 'react-native-canvas-kit'; @@ -30,22 +45,68 @@ const QUADRANTS = [ export default function CanvaScreen() { const [selected, setSelected] = useState(null); + const [exportedUri, setExportedUri] = useState(null); + const stageRef = useRef(null); + + const handleExport = async () => { + const dataUrl = await stageRef.current?.toDataURL({ + mimeType: 'image/png', + }); + if (dataUrl) setExportedUri(dataUrl); + }; + + return ( + <> + setSelected(null)} + onExport={handleExport} + > + {({ width, height }) => ( + + )} + + setExportedUri(null)} /> + + ); +} +function ExportPreview({ + uri, + onClose, +}: { + uri: string | null; + onClose: () => void; +}) { return ( - setSelected(null)} + - {({ width, height }) => ( - - )} - + + + Exported image + {uri && ( + + )} + Tap anywhere to close + + + ); } @@ -54,9 +115,18 @@ interface CanvaBoardProps { height: number; selected: string | null; setSelected: Dispatch>; + stageRef: RefObject; + onExport: () => void; } -function CanvaBoard({ width, height, selected, setSelected }: CanvaBoardProps) { +function CanvaBoard({ + width, + height, + selected, + setSelected, + stageRef, + onExport, +}: CanvaBoardProps) { const quadrantWidth = width / 2; const quadrantHeight = height / 2; const font = useFont(FONT_URL, Math.round(width * 0.2)); @@ -110,7 +180,12 @@ function CanvaBoard({ width, height, selected, setSelected }: CanvaBoardProps) { return ( - + setSelected(null)} width={width} @@ -190,10 +265,64 @@ function CanvaBoard({ width, height, selected, setSelected }: CanvaBoardProps) { + + Export + ); } const styles = StyleSheet.create({ board: { flex: 1 }, + exportButton: { + position: 'absolute', + right: 12, + bottom: 12, + backgroundColor: CANVA_PURPLE, + paddingHorizontal: 18, + paddingVertical: 10, + borderRadius: 22, + shadowColor: '#000', + shadowOpacity: 0.2, + shadowRadius: 8, + shadowOffset: { width: 0, height: 3 }, + elevation: 5, + }, + exportButtonText: { + color: '#ffffff', + fontSize: 14, + fontWeight: '700', + }, + previewBackdrop: { + flex: 1, + backgroundColor: 'rgba(0, 0, 0, 0.75)', + alignItems: 'center', + justifyContent: 'center', + padding: 24, + }, + previewCard: { + width: '100%', + maxWidth: 420, + backgroundColor: '#ffffff', + borderRadius: 16, + padding: 16, + alignItems: 'center', + }, + previewTitle: { + color: '#1b1b1f', + fontSize: 16, + fontWeight: '700', + marginBottom: 12, + }, + previewImage: { + width: '100%', + aspectRatio: 1, + borderRadius: 8, + backgroundColor: '#f0f0f3', + }, + previewHint: { + color: '#9a9aa2', + fontSize: 12, + marginTop: 12, + }, }); diff --git a/example/src/CanvaChrome.tsx b/example/src/CanvaChrome.tsx index dd348f6..5528b85 100644 --- a/example/src/CanvaChrome.tsx +++ b/example/src/CanvaChrome.tsx @@ -30,12 +30,14 @@ interface BoardSize { interface CanvaChromeProps { hasSelection: boolean; onConfirmSelection: () => void; + onExport?: () => void; children: (size: BoardSize) => ReactNode; } export function CanvaChrome({ hasSelection, onConfirmSelection, + onExport, children, }: CanvaChromeProps) { const navigation = useNavigation(); @@ -97,12 +99,9 @@ export function CanvaChrome({ color={HEADER_TINT} style={styles.headerIcon} /> - + + + From 358259967312d56712c1a684dfd8c81e32101ba0 Mon Sep 17 00:00:00 2001 From: adithyavis Date: Fri, 24 Jul 2026 16:36:26 +0530 Subject: [PATCH 3/5] feat: export in docusign example --- example/app/docusign.tsx | 87 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 85 insertions(+), 2 deletions(-) diff --git a/example/app/docusign.tsx b/example/app/docusign.tsx index a7b868e..892972e 100644 --- a/example/app/docusign.tsx +++ b/example/app/docusign.tsx @@ -1,12 +1,19 @@ import { useRef, useState } from 'react'; import { type LayoutChangeEvent, + Image as RNImage, + Modal, Pressable, StyleSheet, Text, View, } from 'react-native'; -import { Stage, BrushLayer, Pen } from 'react-native-canvas-kit'; +import { + Stage, + BrushLayer, + Pen, + type StageHandle, +} from 'react-native-canvas-kit'; import { DrawerButton } from '../src/DrawerButton'; import { useTouchTracker, TouchRings } from '../src/TouchOverlay'; @@ -23,9 +30,11 @@ export default function DocuSignScreen() { const [strokes, setStrokes] = useState([]); const [color, setColor] = useState(DEFAULT_INK); const [pad, setPad] = useState({ width: 0, height: 0 }); + const [signatureUri, setSignatureUri] = useState(null); const strokeCounter = useRef(0); const colorRef = useRef(color); colorRef.current = color; + const stageRef = useRef(null); const { gesture: touchGesture, touches } = useTouchTracker(); const onPadLayout = (e: LayoutChangeEvent) => { @@ -36,6 +45,13 @@ export default function DocuSignScreen() { const clear = () => setStrokes([]); const isEmpty = strokes.length === 0; + const handleCreate = async () => { + const dataUrl = await stageRef.current?.toDataURL({ + mimeType: 'image/png', + }); + if (dataUrl) setSignatureUri(dataUrl); + }; + return ( @@ -51,7 +67,12 @@ export default function DocuSignScreen() { Take Photo - + Create @@ -69,6 +90,7 @@ export default function DocuSignScreen() { {pad.width > 0 && ( + setSignatureUri(null)} + > + setSignatureUri(null)} + > + + Signature created + {signatureUri && ( + + )} + Tap anywhere to close + + + + ); @@ -247,4 +293,41 @@ const styles = StyleSheet.create({ lineHeight: 18, color: '#6b7280', }, + previewBackdrop: { + flex: 1, + backgroundColor: 'rgba(0, 0, 0, 0.6)', + alignItems: 'center', + justifyContent: 'center', + padding: 24, + }, + previewCard: { + width: '100%', + maxWidth: 420, + backgroundColor: '#ffffff', + borderRadius: 16, + padding: 16, + alignItems: 'center', + shadowColor: '#000', + shadowOpacity: 0.2, + shadowRadius: 12, + shadowOffset: { width: 0, height: 4 }, + elevation: 6, + }, + previewTitle: { + color: '#1b1b1f', + fontSize: 16, + fontWeight: '700', + marginBottom: 12, + }, + previewImage: { + width: '100%', + aspectRatio: 1.6, + borderRadius: 8, + backgroundColor: '#f4f5f7', + }, + previewHint: { + color: '#9a9aa2', + fontSize: 12, + marginTop: 12, + }, }); From ec7501d2016246ba484996b4411fdb65df57b04d Mon Sep 17 00:00:00 2001 From: adithyavis Date: Fri, 24 Jul 2026 16:36:27 +0530 Subject: [PATCH 4/5] feat: export in instagram crop example --- example/app/instagram-crop.tsx | 63 +++++++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/example/app/instagram-crop.tsx b/example/app/instagram-crop.tsx index 9450dd3..de882a3 100644 --- a/example/app/instagram-crop.tsx +++ b/example/app/instagram-crop.tsx @@ -1,6 +1,7 @@ import { useEffect, useRef, useState } from 'react'; import { Image as RNImage, + Modal, Pressable, ScrollView, StyleSheet, @@ -17,6 +18,7 @@ import { Layer, Image, type EventObject, + type StageHandle, type TransformResult, } from 'react-native-canvas-kit'; import { CROP_PHOTOS } from '../src/cropPhotos'; @@ -76,8 +78,17 @@ export default function InstagramCropScreen() { const [transforms, setTransforms] = useState>( {} ); + const [croppedUri, setCroppedUri] = useState(null); + const stageRef = useRef(null); const { gesture: touchGesture, touches } = useTouchTracker(); + const handleDone = async () => { + const dataUrl = await stageRef.current?.toDataURL({ + mimeType: 'image/png', + }); + if (dataUrl) setCroppedUri(dataUrl); + }; + const transform = transforms[selectedId] ?? IDENTITY; const selected = CROP_PHOTOS.find((p) => p.id === selectedId)!; @@ -161,7 +172,7 @@ export default function InstagramCropScreen() { Recents - + Done @@ -169,6 +180,7 @@ export default function InstagramCropScreen() { + + setCroppedUri(null)} + > + setCroppedUri(null)} + > + Cropped photo + {croppedUri && ( + + )} + Tap anywhere to close + + ); } @@ -294,4 +331,28 @@ const styles = StyleSheet.create({ borderWidth: 3, borderColor: '#3897f0', }, + previewBackdrop: { + flex: 1, + backgroundColor: 'rgba(0, 0, 0, 0.85)', + alignItems: 'center', + justifyContent: 'center', + padding: 24, + }, + previewTitle: { + color: '#ffffff', + fontSize: 16, + fontWeight: '600', + marginBottom: 20, + }, + previewImage: { + borderRadius: 9999, + borderWidth: 3, + borderColor: '#ffffff', + backgroundColor: '#111111', + }, + previewHint: { + color: '#8e8e93', + fontSize: 12, + marginTop: 20, + }, }); From 112e242fcad39ba63f94fc8d6ec531bf20aab022 Mon Sep 17 00:00:00 2001 From: adithyavis Date: Fri, 24 Jul 2026 16:43:04 +0530 Subject: [PATCH 5/5] docs: add export category --- docs/demos/demos/groups/export.tsx | 124 ++++++++++++++++++ docs/demos/demos/registry.ts | 2 + docs/docs/core-concepts/stage.md | 63 +-------- docs/docs/export/_category_.json | 8 ++ docs/docs/export/overview.md | 93 +++++++++++++ ...index-bd07013b88b73fb72375d7971ba41db2.js} | 3 +- docs/static/demos/index.html | 2 +- 7 files changed, 234 insertions(+), 61 deletions(-) create mode 100644 docs/demos/demos/groups/export.tsx create mode 100644 docs/docs/export/_category_.json create mode 100644 docs/docs/export/overview.md rename docs/static/demos/_expo/static/js/web/{index-647896b5b5e846294f3b74168a61631f.js => index-bd07013b88b73fb72375d7971ba41db2.js} (99%) diff --git a/docs/demos/demos/groups/export.tsx b/docs/demos/demos/groups/export.tsx new file mode 100644 index 0000000..936e099 --- /dev/null +++ b/docs/demos/demos/groups/export.tsx @@ -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(null); + const [uri, setUri] = useState(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 ( + + + + + + {font && ( + + )} + + + + + Export PNG + + + {uri && ( + + Exported image + + + )} + + ); +} + +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 = { + 'export-overview-1': ExportToImage, +}; diff --git a/docs/demos/demos/registry.ts b/docs/demos/demos/registry.ts index 18d834f..b467b4d 100644 --- a/docs/demos/demos/registry.ts +++ b/docs/demos/demos/registry.ts @@ -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 = { ...gettingStartedDemos, @@ -15,4 +16,5 @@ export const DEMOS: Record = { ...stylingDemos, ...interactivityDemos, ...brushesPortalDemos, + ...exportDemos, }; diff --git a/docs/docs/core-concepts/stage.md b/docs/docs/core-concepts/stage.md index aa8856c..fbcc11d 100644 --- a/docs/docs/core-concepts/stage.md +++ b/docs/docs/core-concepts/stage.md @@ -62,65 +62,10 @@ The stage itself is transparent. Give it a background with `style`: ## Exporting to an image -Attach a `ref` to the stage 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). - -On the web a `` 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. - -```tsx -import { useRef } from 'react'; -import { Stage, Layer, Rect, type StageHandle } from 'react-native-canvas-kit'; - -function Scene() { - const stageRef = useRef(null); - - const handleExport = async () => { - const dataUrl = await stageRef.current?.toDataURL(); - // dataUrl: "data:image/png;base64,..." - render in an 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 ( - - - - - - ); -} -``` - -### Methods - -| Method | Returns | Description | -| -------------------- | ----------------------------- | -------------------------------------------------- | -| `makeImageSnapshot` | `Promise` | Raw Skia image for further processing. | -| `toBase64` | `Promise` | Encoded image as a bare base64 string. | -| `toDataURL` | `Promise` | Encoded image as a `data:;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. | +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 diff --git a/docs/docs/export/_category_.json b/docs/docs/export/_category_.json new file mode 100644 index 0000000..629b830 --- /dev/null +++ b/docs/docs/export/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Export", + "position": 9, + "link": { + "type": "doc", + "id": "export/overview" + } +} diff --git a/docs/docs/export/overview.md b/docs/docs/export/overview.md new file mode 100644 index 0000000..5c588ba --- /dev/null +++ b/docs/docs/export/overview.md @@ -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). + + + +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 `` 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(null); + + const handleExport = async () => { + const dataUrl = await stageRef.current?.toDataURL(); + // dataUrl: "data:image/png;base64,..." - render in an 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 ( + + + + + + ); +} +``` + +## Methods + +| Method | Returns | Description | +| -------------------- | ----------------------------- | -------------------------------------------------- | +| `makeImageSnapshot` | `Promise` | Raw Skia image for further processing. | +| `toBase64` | `Promise` | Encoded image as a bare base64 string. | +| `toDataURL` | `Promise` | Encoded image as a `data:;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, +}); +``` diff --git a/docs/static/demos/_expo/static/js/web/index-647896b5b5e846294f3b74168a61631f.js b/docs/static/demos/_expo/static/js/web/index-bd07013b88b73fb72375d7971ba41db2.js similarity index 99% rename from docs/static/demos/_expo/static/js/web/index-647896b5b5e846294f3b74168a61631f.js rename to docs/static/demos/_expo/static/js/web/index-bd07013b88b73fb72375d7971ba41db2.js index 1138c34..c802af8 100644 --- a/docs/static/demos/_expo/static/js/web/index-647896b5b5e846294f3b74168a61631f.js +++ b/docs/static/demos/_expo/static/js/web/index-bd07013b88b73fb72375d7971ba41db2.js @@ -946,7 +946,7 @@ __d(function(g,r,i,a,m,e,d){"use strict";Object.defineProperty(e,'__esModule',{v __d(function(g,r,i,a,m,_e,_d){"use strict";function e(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(_e,'__esModule',{value:!0}),Object.defineProperty(_e,"default",{enumerable:!0,get:function(){return V}});var t=r(_d[0]),n=(function(e){if(e&&e.__esModule)return e;var t={};return e&&Object.keys(e).forEach(function(n){var s=Object.getOwnPropertyDescriptor(e,n);Object.defineProperty(t,n,s.get?s:{enumerable:!0,get:function(){return e[n]}})}),t.default=e,t})(t),s=e(r(_d[1])),o=e(r(_d[2])),d=e(r(_d[3])),l=e(r(_d[4])),h=e(r(_d[5])),u=e(r(_d[6])),p=e(r(_d[7])),c=r(_d[8]),w=r(_d[9]),f=r(_d[10]),v=r(_d[11]);const S='Idle',y='Dragging',b='Settling';class V extends t.Component{static defaultProps={drawerWidth:200,drawerPosition:'left',useNativeAnimations:!0,drawerType:'front',edgeWidth:20,minSwipeDistance:3,overlayColor:'rgba(0, 0, 0, 0.7)',drawerLockMode:'unlocked',enableTrackpadTwoFingerGesture:!1};constructor(e){super(e);const t=new o.default.Value(0),n=new o.default.Value(0),s=new o.default.Value(0);this.state={dragX:t,touchX:n,drawerTranslation:s,containerWidth:0,drawerState:S,drawerOpened:!1},this.updateAnimatedEvent(e,this.state)}shouldComponentUpdate(e,t){return this.props.drawerPosition===e.drawerPosition&&this.props.drawerWidth===e.drawerWidth&&this.props.drawerType===e.drawerType&&this.state.containerWidth===t.containerWidth||this.updateAnimatedEvent(e,t),!0}accessibilityIsModalView=n.createRef();pointerEventsView=n.createRef();panGestureHandler=n.createRef();drawerShown=!1;static positions={Left:'left',Right:'right'};updateAnimatedEvent=(e,t)=>{const{drawerPosition:n,drawerWidth:s,drawerType:d}=e,{dragX:l,touchX:h,drawerTranslation:u,containerWidth:p}=t;let c=l,w=h;'left'!==n?(c=o.default.multiply(new o.default.Value(-1),l),w=o.default.add(new o.default.Value(p),o.default.multiply(new o.default.Value(-1),h)),h.setValue(p)):h.setValue(0);let f=c;if('front'===d){const e=o.default.add(w,o.default.multiply(new o.default.Value(-1),c)).interpolate({inputRange:[s-1,s,s+1],outputRange:[0,0,1]});f=o.default.add(c,e)}this.openValue=o.default.add(f,u).interpolate({inputRange:[0,s],outputRange:[0,1],extrapolate:'clamp'});const v={useNativeDriver:e.useNativeAnimations};this.props.onDrawerSlide&&(v.listener=e=>{const t=Math.floor(Math.abs(e.nativeEvent.translationX))/this.state.containerWidth;this.props.onDrawerSlide?.(t)}),this.onGestureEvent=o.default.event([{nativeEvent:{translationX:l,x:h}}],v)};handleContainerLayout=({nativeEvent:e})=>{this.setState({containerWidth:e.layout.width})};emitStateChanged=(e,t)=>{this.props.onDrawerStateChanged?.(e,t)};openingHandlerStateChange=({nativeEvent:e})=>{e.oldState===f.State.ACTIVE?this.handleRelease({nativeEvent:e}):e.state===f.State.ACTIVE&&(this.emitStateChanged(y,!1),this.setState({drawerState:y}),'on-drag'===this.props.keyboardDismissMode&&h.default.dismiss(),this.props.hideStatusBar&&u.default.setHidden(!0,this.props.statusBarAnimation||'slide'))};onTapHandlerStateChange=({nativeEvent:e})=>{this.drawerShown&&e.oldState===f.State.ACTIVE&&'locked-open'!==this.props.drawerLockMode&&this.closeDrawer()};handleRelease=({nativeEvent:e})=>{const{drawerWidth:t,drawerPosition:n,drawerType:s}=this.props,{containerWidth:o}=this.state;let{translationX:d,velocityX:l,x:h}=e;'left'!==n&&(d=-d,h=o-h,l=-l);const u=h-d;let p=0;'front'===s&&(p=u>t?u-t:0);const c=d+p+(this.drawerShown?t:0);c+.05*l>t/2?this.animateDrawer(c,t,l):this.animateDrawer(c,0,l)};updateShowing=e=>{this.drawerShown=e,this.accessibilityIsModalView.current?.setNativeProps({accessibilityViewIsModal:e}),this.pointerEventsView.current?.setNativeProps({pointerEvents:e?'auto':'none'});const{drawerPosition:t,minSwipeDistance:n,edgeWidth:s}=this.props,o='left'===t,d=(o?1:-1)*(this.drawerShown?-1:1),l=o?{left:0,width:e?void 0:s}:{right:0,width:e?void 0:s};this.panGestureHandler.current?.setNativeProps({hitSlop:l,activeOffsetX:d*n})};animateDrawer=(e,t,n,s)=>{if(this.state.dragX.setValue(0),this.state.touchX.setValue('left'===this.props.drawerPosition?0:this.state.containerWidth),null!=e){let s=e;this.props.useNativeAnimations&&(e0?s=Math.min(e+n/60,t):e>t&&n<0&&(s=Math.max(e+n/60,t))),this.state.drawerTranslation.setValue(s)}const d=0!==t;this.updateShowing(d),this.emitStateChanged(b,d),this.setState({drawerState:b}),this.props.hideStatusBar&&u.default.setHidden(d,this.props.statusBarAnimation||'slide'),o.default.spring(this.state.drawerTranslation,{velocity:n,bounciness:0,toValue:t,useNativeDriver:this.props.useNativeAnimations,speed:s??void 0}).start(({finished:e})=>{e&&(this.emitStateChanged(S,d),this.setState({drawerOpened:d}),this.state.drawerState!==y&&this.setState({drawerState:S}),d?this.props.onDrawerOpen?.():this.props.onDrawerClose?.())})};openDrawer=(e={})=>{this.animateDrawer(void 0,this.props.drawerWidth,e.velocity?e.velocity:0,e.speed),this.forceUpdate()};closeDrawer=(e={})=>{this.animateDrawer(void 0,0,e.velocity?e.velocity:0,e.speed),this.forceUpdate()};renderOverlay=()=>{let e;(0,s.default)(this.openValue,'should be set'),e=this.state.drawerState!==S?this.openValue:this.state.drawerOpened?1:0;const t={opacity:e,backgroundColor:this.props.overlayColor};return(0,v.jsx)(w.TapGestureHandler,{onHandlerStateChange:this.onTapHandlerStateChange,children:(0,v.jsx)(o.default.View,{pointerEvents:this.drawerShown?'auto':'none',ref:this.pointerEventsView,style:[C.overlay,t]})})};renderDrawer=()=>{const{drawerBackgroundColor:e,drawerWidth:t,drawerPosition:n,drawerType:d,drawerContainerStyle:h,contentContainerStyle:u}=this.props,c='left'===n,w='back'!==d,f='front'!==d,y=p.default.isRTL?c:!c,b={backgroundColor:e,width:t},V=this.openValue;let O;if((0,s.default)(V,'should be set'),f){O={transform:[{translateX:V.interpolate({inputRange:[0,1],outputRange:c?[0,t]:[0,-t],extrapolate:'clamp'})}]}}let D=0;if(w){const e=c?-t:t;D=this.state.drawerState!==S?V.interpolate({inputRange:[0,1],outputRange:[e,0],extrapolate:'clamp'}):this.state.drawerOpened?0:e}const x={transform:[{translateX:D}],flexDirection:y?'row-reverse':'row'};return(0,v.jsxs)(o.default.View,{style:C.main,onLayout:this.handleContainerLayout,children:[(0,v.jsxs)(o.default.View,{style:['front'===d?C.containerOnBack:C.containerInFront,O,u],importantForAccessibility:this.drawerShown?'no-hide-descendants':'yes',children:['function'==typeof this.props.children?this.props.children(this.openValue):this.props.children,this.renderOverlay()]}),(0,v.jsx)(o.default.View,{pointerEvents:"box-none",ref:this.accessibilityIsModalView,accessibilityViewIsModal:this.drawerShown,style:[C.drawerContainer,x,h],children:(0,v.jsx)(l.default,{style:b,children:this.props.renderNavigationView(this.openValue)})})]})};setPanGestureRef=e=>{this.panGestureHandler.current=e,this.props.onGestureRef?.(e)};render(){const{drawerPosition:e,drawerLockMode:t,edgeWidth:n,minSwipeDistance:s}=this.props,o='left'===e,d=(o?1:-1)*(this.drawerShown?-1:1),l=o?{left:0,width:this.drawerShown?void 0:n}:{right:0,width:this.drawerShown?void 0:n};return(0,v.jsx)(c.PanGestureHandler,{userSelect:this.props.userSelect,activeCursor:this.props.activeCursor,mouseButton:this.props.mouseButton,enableContextMenu:this.props.enableContextMenu,ref:this.setPanGestureRef,hitSlop:l,activeOffsetX:d*s,failOffsetY:[-15,15],onGestureEvent:this.onGestureEvent,onHandlerStateChange:this.openingHandlerStateChange,enableTrackpadTwoFingerGesture:this.props.enableTrackpadTwoFingerGesture,enabled:'locked-closed'!==t&&'locked-open'!==t,children:this.renderDrawer()})}}const C=d.default.create({drawerContainer:Object.assign({},d.default.absoluteFillObject,{zIndex:1001,flexDirection:'row'}),containerInFront:Object.assign({},d.default.absoluteFillObject,{zIndex:1002}),containerOnBack:Object.assign({},d.default.absoluteFillObject),main:{flex:1,zIndex:0,overflow:'hidden'},overlay:Object.assign({},d.default.absoluteFillObject,{zIndex:1e3})})},795,[6,220,726,130,291,796,797,788,372,361,232,359]); __d(function(g,r,i,a,m,_e,d){"use strict";Object.defineProperty(_e,'__esModule',{value:!0}),Object.defineProperty(_e,"default",{enumerable:!0,get:function(){return n}});var e,t=r(d[0]),s=(e=t)&&e.__esModule?e:{default:e},n={isVisible:()=>!1,addListener:()=>({remove:()=>{}}),dismiss(){(0,s.default)()},removeAllListeners(){},removeListener(){}}},796,[328]); __d(function(g,r,i,a,m,e,d){"use strict";Object.defineProperty(e,'__esModule',{value:!0}),Object.defineProperty(e,"default",{enumerable:!0,get:function(){return u}});var t=()=>{};function n(){return null}n.setBackgroundColor=t,n.setBarStyle=t,n.setHidden=t,n.setNetworkActivityIndicatorVisible=t,n.setTranslucent=t;var u=n},797,[]); -__d(function(g,r,i,a,m,e,d){"use strict";Object.defineProperty(e,'__esModule',{value:!0}),Object.defineProperty(e,"DEMOS",{enumerable:!0,get:function(){return b}});var t=r(d[0]),s=r(d[1]),o=r(d[2]),n=r(d[3]),c=r(d[4]),u=r(d[5]),D=r(d[6]);const b=Object.assign({},t.gettingStartedDemos,s.coreConceptsDemos,o.shapesDemos,n.shapesExtraDemos,c.stylingDemos,u.interactivityDemos,D.brushesPortalDemos)},798,[799,1084,1102,1103,1117,1118,1119]); +__d(function(g,r,i,a,m,e,d){"use strict";Object.defineProperty(e,'__esModule',{value:!0}),Object.defineProperty(e,"DEMOS",{enumerable:!0,get:function(){return b}});var t=r(d[0]),s=r(d[1]),o=r(d[2]),n=r(d[3]),c=r(d[4]),D=r(d[5]),u=r(d[6]),p=r(d[7]);const b=Object.assign({},t.gettingStartedDemos,s.coreConceptsDemos,o.shapesDemos,n.shapesExtraDemos,c.stylingDemos,D.interactivityDemos,u.brushesPortalDemos,p.exportDemos)},798,[799,1084,1102,1103,1117,1118,1119,1127]); __d(function(g,r,i,a,m,_e,d){"use strict";function e(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(_e,'__esModule',{value:!0}),Object.defineProperty(_e,"gettingStartedDemos",{enumerable:!0,get:function(){return x}});var t=r(d[0]),l=e(r(d[1])),n=e(r(d[2])),s=e(r(d[3])),o=e(r(d[4])),c=r(d[5]),h=r(d[6]),u=r(d[7]);const f=l.default.create({root:{flex:1},stage:{backgroundColor:'#faf7ff'},badge:{position:'absolute',bottom:16,left:0,right:0,alignItems:'center'},badgeText:{fontSize:14,fontWeight:'600',color:'#6D28D9',backgroundColor:'#ffffffcc',paddingHorizontal:12,paddingVertical:6,borderRadius:999,overflow:'hidden'}}),x={'quick-start-1':function(){return(0,u.jsxs)(h.DemoStage,{logicalWidth:320,logicalHeight:280,children:[(0,u.jsx)(c.Rect,{x:30,y:40,width:140,height:90,cornerRadius:16,fill:"#8a2be2"}),(0,u.jsx)(c.Circle,{x:230,y:170,radius:60,fill:"#ff5aa5",stroke:"#1b0030",strokeWidth:6})]})},'quick-start-2':function(){const{width:e,height:t}=(0,o.default)();return(0,u.jsx)(s.default,{style:f.root,children:(0,u.jsx)(c.Stage,{width:e,height:t,style:f.stage,children:(0,u.jsx)(c.Layer,{width:e,height:t,gestureEnabled:!0,children:(0,u.jsx)(c.Circle,{x:e/2,y:t/2,radius:60,fill:"#ff5aa5",draggable:!0})})})})},'quick-start-3':function(){const{width:e,height:l}=(0,o.default)(),[h,x]=(0,t.useState)(null);return(0,u.jsxs)(s.default,{style:f.root,children:[(0,u.jsx)(c.Stage,{width:e,height:l,style:f.stage,children:(0,u.jsx)(c.Layer,{width:e,height:l,gestureEnabled:!0,children:(0,u.jsx)(c.Circle,{x:e/2,y:l/2,radius:60,fill:"#ff5aa5",draggable:!0,onDragEnd:e=>{const{x:t,y:l}=e.currentTarget.getAbsolutePosition();x({x:Math.round(t),y:Math.round(l)})}})})}),(0,u.jsx)(s.default,{style:f.badge,pointerEvents:"none",children:(0,u.jsx)(n.default,{style:f.badgeText,children:h?`Dropped at ${h.x}, ${h.y}`:'Drag the circle'})})]})},'quick-start-4':function(){const{width:e,height:l}=(0,o.default)(),[h,x]=(0,t.useState)(null),[b,y]=(0,t.useState)({x:e/2-80,y:l/2-55,scaleX:1,scaleY:1,rotation:0});return(0,u.jsxs)(s.default,{style:f.root,children:[(0,u.jsx)(c.Stage,{width:e,height:l,style:f.stage,children:(0,u.jsxs)(c.Layer,{onTap:()=>x(null),width:e,height:l,gestureEnabled:!0,children:[(0,u.jsx)(c.Rect,{id:"box",x:b.x,y:b.y,scaleX:b.scaleX,scaleY:b.scaleY,rotation:b.rotation,width:160,height:110,cornerRadius:12,fill:"#22d3ee",draggable:!0,rotatable:!0,scalable:!0,onTap:e=>{x('#box'),e.cancelBubble=!0}}),(0,u.jsx)(c.Transformer,{node:h,onTransformEnd:e=>y({x:e.x,y:e.y,scaleX:e.scaleX,scaleY:e.scaleY,rotation:e.rotation})})]})}),(0,u.jsx)(s.default,{style:f.badge,pointerEvents:"none",children:(0,u.jsx)(n.default,{style:f.badgeText,children:"Tap the rectangle to attach a Transformer"})})]})}}},799,[6,130,650,291,800,801,1083,359]); __d(function(g,r,i,a,m,_e,d){"use strict";Object.defineProperty(_e,'__esModule',{value:!0}),Object.defineProperty(_e,"default",{enumerable:!0,get:function(){return f}});var e,t=r(d[0]),n=(e=t)&&e.__esModule?e:{default:e},u=r(d[1]);function f(){var e=(0,u.useState)(()=>n.default.get('window')),t=e[0],f=e[1];return(0,u.useEffect)(()=>{function e(e){var t=e.window;null!=t&&f(t)}return n.default.addEventListener('change',e),f(n.default.get('window')),()=>{n.default.removeEventListener('change',e)}},[]),t}},800,[279,6]); __d(function(g,r,i,a,m,e,d){"use strict";Object.defineProperty(e,'__esModule',{value:!0}),Object.defineProperty(e,"Stage",{enumerable:!0,get:function(){return n.Stage}}),Object.defineProperty(e,"Layer",{enumerable:!0,get:function(){return u.Layer}}),Object.defineProperty(e,"Group",{enumerable:!0,get:function(){return o.Group}}),Object.defineProperty(e,"Rect",{enumerable:!0,get:function(){return c.Rect}}),Object.defineProperty(e,"Circle",{enumerable:!0,get:function(){return f.Circle}}),Object.defineProperty(e,"Ellipse",{enumerable:!0,get:function(){return b.Ellipse}}),Object.defineProperty(e,"Line",{enumerable:!0,get:function(){return l.Line}}),Object.defineProperty(e,"RegularPolygon",{enumerable:!0,get:function(){return P.RegularPolygon}}),Object.defineProperty(e,"Star",{enumerable:!0,get:function(){return p.Star}}),Object.defineProperty(e,"Text",{enumerable:!0,get:function(){return y.Text}}),Object.defineProperty(e,"Image",{enumerable:!0,get:function(){return j.Image}}),Object.defineProperty(e,"Transformer",{enumerable:!0,get:function(){return O.Transformer}}),Object.defineProperty(e,"SnapGrid",{enumerable:!0,get:function(){return s.SnapGrid}}),Object.defineProperty(e,"Portal",{enumerable:!0,get:function(){return S.Portal}}),Object.defineProperty(e,"BrushLayer",{enumerable:!0,get:function(){return R.BrushLayer}}),Object.defineProperty(e,"Pen",{enumerable:!0,get:function(){return R.Pen}}),Object.defineProperty(e,"Pencil",{enumerable:!0,get:function(){return R.Pencil}}),Object.defineProperty(e,"Marker",{enumerable:!0,get:function(){return R.Marker}}),Object.defineProperty(e,"Highlighter",{enumerable:!0,get:function(){return R.Highlighter}}),Object.defineProperty(e,"Tape",{enumerable:!0,get:function(){return R.Tape}}),Object.defineProperty(e,"Eraser",{enumerable:!0,get:function(){return R.Eraser}}),Object.defineProperty(e,"BRUSH_PATHS",{enumerable:!0,get:function(){return R.BRUSH_PATHS}}),Object.defineProperty(e,"BRUSHES",{enumerable:!0,get:function(){return R.BRUSHES}}),Object.defineProperty(e,"useImage",{enumerable:!0,get:function(){return h.useImage}}),Object.defineProperty(e,"useFont",{enumerable:!0,get:function(){return h.useFont}}),Object.defineProperty(e,"matchFont",{enumerable:!0,get:function(){return h.matchFont}});var t=r(d[0]),n=r(d[1]),u=r(d[2]),o=r(d[3]),c=r(d[4]),f=r(d[5]),b=r(d[6]),l=r(d[7]),P=r(d[8]),p=r(d[9]),y=r(d[10]),j=r(d[11]),O=r(d[12]),s=r(d[13]),S=r(d[14]),R=r(d[15]),h=r(d[16]);(0,t.assertReanimatedVersion)()},801,[802,804,1055,1057,1058,1061,1062,1063,1065,1067,1068,1069,1070,1076,1077,1078,805]); @@ -1305,5 +1305,6 @@ __d(function(g,r,i,a,m,_e,d){'use strict';Object.defineProperty(_e,'__esModule', __d(function(g,r,i,a,m,_e,d){"use strict";function e(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(_e,'__esModule',{value:!0}),Object.defineProperty(_e,"default",{enumerable:!0,get:function(){return l}});var t=r(d[0]),n=e(r(d[1])),u=e(r(d[2]));function l(e,l){var f=(0,u.default)(()=>new Map),c=(0,u.default)(()=>(n,u)=>{var c=f.get(n);null!=c&&c(),null==u&&(f.delete(n),u=()=>{});var o=(0,t.addEventListener)(n,e,u,l);return f.set(n,o),o});return(0,n.default)(()=>()=>{f.forEach(e=>{e()}),f.clear()},[f]),c}},1124,[1123,303,312]); __d(function(g,r,i,a,m,_e,d){'use strict';Object.defineProperty(_e,'__esModule',{value:!0}),Object.defineProperty(_e,"default",{enumerable:!0,get:function(){return f}});var e,u=r(d[0]),t=(e=u)&&e.__esModule?e:{default:e},n=r(d[1]);function f(e,u){var f=(0,n.useRef)(null);null==f.current&&(f.current=new t.default(u));var l=f.current;return(0,n.useEffect)(()=>{l.configure(u)},[u,l]),(0,n.useEffect)(()=>()=>{l.reset()},[l]),(0,n.useDebugValue)(u),l.getEventHandlers()}},1125,[1126,6]); __d(function(g,r,i,a,m,e,d){'use strict';Object.defineProperty(e,'__esModule',{value:!0}),Object.defineProperty(e,"default",{enumerable:!0,get:function(){return N}});var t='DELAY',s='ERROR',n='LONG_PRESS_DETECTED',o='NOT_RESPONDER',l='RESPONDER_ACTIVE_LONG_PRESS_START',_='RESPONDER_ACTIVE_PRESS_START',E='RESPONDER_INACTIVE_PRESS_START',u='RESPONDER_RELEASE',h='RESPONDER_TERMINATED',c=Object.freeze({NOT_RESPONDER:{DELAY:s,RESPONDER_GRANT:E,RESPONDER_RELEASE:s,RESPONDER_TERMINATED:s,LONG_PRESS_DETECTED:s},RESPONDER_INACTIVE_PRESS_START:{DELAY:_,RESPONDER_GRANT:s,RESPONDER_RELEASE:o,RESPONDER_TERMINATED:o,LONG_PRESS_DETECTED:s},RESPONDER_ACTIVE_PRESS_START:{DELAY:s,RESPONDER_GRANT:s,RESPONDER_RELEASE:o,RESPONDER_TERMINATED:o,LONG_PRESS_DETECTED:l},RESPONDER_ACTIVE_LONG_PRESS_START:{DELAY:s,RESPONDER_GRANT:s,RESPONDER_RELEASE:o,RESPONDER_TERMINATED:o,LONG_PRESS_DETECTED:l},ERROR:{DELAY:o,RESPONDER_GRANT:E,RESPONDER_RELEASE:o,RESPONDER_TERMINATED:o,LONG_PRESS_DETECTED:o}}),R=t=>t.getAttribute('role'),T=t=>t.tagName.toLowerCase(),P=t=>t===_||t===l,S=t=>'button'===R(t),D=t=>t===E||t===_||t===l,v=t=>t===h||t===u,p=t=>{var s=t.key,n=t.target,o=' '===s||'Spacebar'===s,l='button'===T(n)||S(n);return'Enter'===s||o&&l};class N{constructor(t){this._eventHandlers=null,this._isPointerTouch=!1,this._longPressDelayTimeout=null,this._longPressDispatched=!1,this._pressDelayTimeout=null,this._pressOutDelayTimeout=null,this._touchState=o,this._responderElement=null,this.configure(t)}configure(t){this._config=t}reset(){this._cancelLongPressDelayTimeout(),this._cancelPressDelayTimeout(),this._cancelPressOutDelayTimeout()}getEventHandlers(){return null==this._eventHandlers&&(this._eventHandlers=this._createEventHandlers()),this._eventHandlers}_createEventHandlers(){var s=(s,n)=>{s.persist(),this._cancelPressOutDelayTimeout(),this._longPressDispatched=!1,this._selectionTerminated=!1,this._touchState=o,this._isPointerTouch='touchstart'===s.nativeEvent.type,this._receiveSignal("RESPONDER_GRANT",s);var l=O(this._config.delayPressStart,0,50);!1!==n&&l>0?this._pressDelayTimeout=setTimeout(()=>{this._receiveSignal(t,s)},l):this._receiveSignal(t,s);var _=O(this._config.delayLongPress,10,450);this._longPressDelayTimeout=setTimeout(()=>{this._handleLongPress(s)},_+l)},n=t=>{this._receiveSignal(u,t)},l=t=>{var s=this._config.onPress,_=t.target;if(this._touchState!==o&&p(t)){n(t),document.removeEventListener('keyup',l);var E=_.getAttribute('role'),u=T(_),h='link'===E||'a'===u||'button'===u||'input'===u||'select'===u||'textarea'===u,c=this._responderElement===_;null!=s&&!h&&c&&s(t),this._responderElement=null}};return{onStartShouldSetResponder:t=>{var s=this._config.disabled;return s&&S(t.currentTarget)&&t.stopPropagation(),null==s||!s},onKeyDown:t=>{var n=this._config.disabled,_=t.key,E=t.target;if(!n&&p(t)){this._touchState===o&&(s(t,!1),this._responderElement=E,document.addEventListener('keyup',l));var u=' '===_||'Spacebar'===_,h=R(E);u&&('button'===h||'menuitem'===h)&&'button'!==T(E)&&t.preventDefault(),t.stopPropagation()}},onResponderGrant:t=>s(t),onResponderMove:t=>{null!=this._config.onPressMove&&this._config.onPressMove(t);var s=f(t);if(null!=this._touchActivatePosition){var n=this._touchActivatePosition.pageX-s.pageX,o=this._touchActivatePosition.pageY-s.pageY;Math.hypot(n,o)>10&&this._cancelLongPressDelayTimeout()}},onResponderRelease:t=>n(t),onResponderTerminate:t=>{'selectionchange'===t.nativeEvent.type&&(this._selectionTerminated=!0),this._receiveSignal(h,t)},onResponderTerminationRequest:t=>{var s=this._config,n=s.cancelable,o=s.disabled,l=s.onLongPress;return!(!o&&null!=l&&this._isPointerTouch&&'contextmenu'===t.nativeEvent.type)&&(null==n||n)},onClick:t=>{var s=this._config,n=s.disabled,o=s.onPress;n?S(t.currentTarget)&&t.stopPropagation():(t.stopPropagation(),this._longPressDispatched||this._selectionTerminated?t.preventDefault():null!=o&&!1===t.altKey&&o(t))},onContextMenu:t=>{var s=this._config,n=s.disabled,o=s.onLongPress;n?S(t.currentTarget)&&t.stopPropagation():null!=o&&this._isPointerTouch&&!t.defaultPrevented&&(t.preventDefault(),t.stopPropagation())}}}_receiveSignal(t,n){var l=this._touchState,_=null;null!=c[l]&&(_=c[l][t]),this._touchState===o&&t===u||(null==_||_===s?console.error("PressResponder: Invalid signal "+t+" for state "+l+" on responder"):l!==_&&(this._performTransitionSideEffects(l,_,t,n),this._touchState=_))}_performTransitionSideEffects(t,s,o,_){if(v(o)&&(setTimeout(()=>{this._isPointerTouch=!1},0),this._touchActivatePosition=null,this._cancelLongPressDelayTimeout()),D(t)&&o===n){var E=this._config.onLongPress;null!=E&&null==_.nativeEvent.key&&(E(_),this._longPressDispatched=!0)}var h=P(t),c=P(s);if(!h&&c?this._activate(_):h&&!c&&this._deactivate(_),D(t)&&o===u){var R=this._config,T=R.onLongPress;if(null!=R.onPress)null!=T&&t===l||c||h||(this._activate(_),this._deactivate(_))}this._cancelPressDelayTimeout()}_activate(t){var s=this._config,n=s.onPressChange,o=s.onPressStart,l=f(t);this._touchActivatePosition={pageX:l.pageX,pageY:l.pageY},null!=o&&o(t),null!=n&&n(!0)}_deactivate(t){var s=this._config,n=s.onPressChange,o=s.onPressEnd;function l(){null!=o&&o(t),null!=n&&n(!1)}var _=O(this._config.delayPressEnd);_>0?this._pressOutDelayTimeout=setTimeout(()=>{l()},_):l()}_handleLongPress(t){this._touchState!==_&&this._touchState!==l||this._receiveSignal(n,t)}_cancelLongPressDelayTimeout(){null!=this._longPressDelayTimeout&&(clearTimeout(this._longPressDelayTimeout),this._longPressDelayTimeout=null)}_cancelPressDelayTimeout(){null!=this._pressDelayTimeout&&(clearTimeout(this._pressDelayTimeout),this._pressDelayTimeout=null)}_cancelPressOutDelayTimeout(){null!=this._pressOutDelayTimeout&&(clearTimeout(this._pressOutDelayTimeout),this._pressOutDelayTimeout=null)}}function O(t,s,n){return void 0===s&&(s=0),void 0===n&&(n=0),Math.max(s,null!=t?t:n)}function f(t){var s=t.nativeEvent,n=s.changedTouches,o=s.touches;return null!=o&&o.length>0?o[0]:null!=n&&n.length>0?n[0]:t.nativeEvent}},1126,[]); +__d(function(g,r,i,a,m,_e,d){"use strict";function e(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(_e,'__esModule',{value:!0}),Object.defineProperty(_e,"exportDemos",{enumerable:!0,get:function(){return b}});var t=r(d[0]),o=e(r(d[1])),n=e(r(d[2])),l=e(r(d[3])),s=e(r(d[4])),f=e(r(d[5])),u=e(r(d[6])),c=r(d[7]),h=r(d[8]),x=r(d[9]);const p=l.default.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'}}),b={'export-overview-1':function(){const{width:e,height:l}=(0,u.default)(),b=(0,c.useFont)(h.FONT_URL,30),y=(0,t.useRef)(null),[j,w]=(0,t.useState)(null),v=e/2,R=l/2;return(0,x.jsxs)(f.default,{style:p.root,children:[(0,x.jsx)(c.Stage,{ref:y,width:e,height:l,style:p.stage,children:(0,x.jsxs)(c.Layer,{width:e,height:l,gestureEnabled:!0,children:[(0,x.jsx)(c.Rect,{x:v-130,y:R-70,width:150,height:110,cornerRadius:16,fill:"#8a2be2",draggable:!0}),(0,x.jsx)(c.Circle,{x:v+70,y:R,radius:54,fill:"#22d3ee",draggable:!0}),b&&(0,x.jsx)(c.Text,{text:"Drag, then Export",x:v-120,y:R+90,font:b,fill:"#1b0030"})]})}),(0,x.jsx)(n.default,{style:p.exportButton,onPress:async()=>{const e=await(y.current?.toDataURL({mimeType:'image/png'}));e&&w(e)},hitSlop:8,children:(0,x.jsx)(s.default,{style:p.exportButtonText,children:"Export PNG"})}),j&&(0,x.jsxs)(f.default,{style:p.preview,pointerEvents:"none",children:[(0,x.jsx)(s.default,{style:p.previewLabel,children:"Exported image"}),(0,x.jsx)(o.default,{source:{uri:j},style:p.previewImage,resizeMode:"cover"})]})]})}}},1127,[6,627,1120,130,650,291,800,801,1085,359]); __r(91); __r(0); \ No newline at end of file diff --git a/docs/static/demos/index.html b/docs/static/demos/index.html index b27a487..2d675c3 100644 --- a/docs/static/demos/index.html +++ b/docs/static/demos/index.html @@ -32,6 +32,6 @@
- +