Add Docusaurus documentation site with i18n support

- Docusaurus 3.7.0 with both English and Chinese (zh-CN) locales
- Full API reference split into structured pages
- Homepage with hero, feature cards, and quick start section
- GitHub Actions workflow for auto-deployment to GitHub Pages

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
a1518 2026-07-02 23:25:56 -07:00
parent f7fd70005d
commit bea6de3767
61 changed files with 20971 additions and 0 deletions

41
.github/workflows/deploy-docs.yml vendored Normal file
View File

@ -0,0 +1,41 @@
name: Deploy Docs to GitHub Pages
on:
push:
branches: [main]
paths:
- 'docs/**'
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: 'pages'
cancel-in-progress: false
jobs:
deploy:
runs-on: ubuntu-latest
defaults:
run:
working-directory: docs
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
cache-dependency-path: docs/package-lock.json
- run: npm ci
- run: npm run build
- uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: docs/build
publish_branch: gh-pages

5
docs/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
# Docusaurus docs site
node_modules/
.docusaurus/
build/
.cache/

View File

@ -0,0 +1,72 @@
---
id: callbacks
title: "Props: Callbacks"
---
# 📞 Props: Callbacks
| Prop | Signature | Description |
| --- | --- | --- |
| `onWatch` | `(state, durationMs, detail?) => void` | Initialization stage callback; `durationMs` is relative to this `init` start |
| `onPaintCallback` | `(payload: PaintCallbackPayload) => void` | Fires on successful paint, or when tapping a region without a brush selected |
| `onError` | `(message, error?) => void` | Segmentation or loading failure |
## PaintCallbackPayload
(Discriminated union, distinguished by `payload.kind`):
```ts
// Successful paint
{
kind: 'painted';
regionId: number;
regionName: string;
color: BgrColor;
configJson?: Record<string, unknown>; // from setPaintColor / initialPaintConfigJson
}
// Tapped a valid region without selecting a brush (no paint performed)
{
kind: 'brush_required';
hint: string; // e.g. "Please select a brush color first (bottom color bar or ref.setPaintColor)"
regionId: number;
regionName: string;
}
```
Example:
```tsx
onPaintCallback={payload => {
if (payload.kind === 'brush_required') {
showToast(payload.hint);
return;
}
savePaintRecord(payload.regionId, payload.color, payload.configJson);
}}
```
## onWatch detail (MaskSegmentWatchDetail)
| Field | Type | Description |
| --- | --- | --- |
| `regionCount` | `number` | Current effective region count |
| `maskPathsReady` | `boolean` | Whether outline Skia paths are ready |
| `freqLayersReady` | `boolean` | Whether frequency Shader textures are ready |
| `errorMessage` | `string` | Failure description in `error` state |
### onWatch State Flow
```
init
→ images_loaded Origin + mask read complete
→ mask_aligned Mask dimensions aligned
→ mask_sampled Mask pixel sampling complete
→ regions_ready Region extraction succeeded
→ layers_ready Paint texture layers ready (detail.maskPathsReady may still be false)
→ interactive Interactive (can tap regions, select colors, paint)
→ mask_paths_ready Outline paths ready (carousel dashed outlines can display; detail.maskPathsReady is true)
→ error Failure (detail.errorMessage has description)
```
`layers_ready` / `interactive` may fire before outline paths finish computing. If the host dismisses a blocking loader at `interactive`, the user can already operate; carousel dashed outlines appear automatically after `mask_paths_ready`.

62
docs/docs/api/index.md Normal file
View File

@ -0,0 +1,62 @@
---
id: index
title: API Reference
---
# 📖 API Reference
## Imports
```tsx
import MaskSegmentCanvas, {
type MaskSegmentCanvasRef,
type MaskSegmentCanvasProps,
type MaskSegmentSession,
type MaskSegmentWatchState,
type MaskSegmentWatchDetail,
type BgrColor,
type MaskSemanticColor,
type PaintCallbackPayload,
type PaintedRegionRecord,
type PipelineConfig,
type MaskSegmentConfig,
type PaintConfig,
type InteractionConfig,
type SavePaintResult,
MASK_SEMANTIC_COLORS,
BASEBOARD_SEMANTIC_NAME,
prewarmPngBgrCacheAsync,
DEFAULT_PIPELINE_CONFIG,
DEFAULT_MASK_CONFIG,
DEFAULT_PAINT_CONFIG,
DEFAULT_INTERACTION_CONFIG,
} from 'react-native-mask-segment-canvas';
```
| Category | Names |
| --- | --- |
| Component | `MaskSegmentCanvas` (default) |
| Ref / Props types | `MaskSegmentCanvasRef`, `MaskSegmentCanvasProps` |
| Session / callback types | `MaskSegmentSession`, `PaintCallbackPayload`, `PaintedRegionRecord`, `SavePaintResult` |
| Watch types | `MaskSegmentWatchState`, `MaskSegmentWatchDetail` |
| Config types | `PipelineConfig`, `MaskSegmentConfig`, `PaintConfig`, `InteractionConfig` |
| Semantic colors | `MASK_SEMANTIC_COLORS`, `BASEBOARD_SEMANTIC_NAME` |
| Utilities | `prewarmPngBgrCacheAsync` |
| Runtime defaults | `DEFAULT_*_CONFIG` |
---
## Props Overview
| Category | Description |
| --- | --- |
| [Image & Initialization](/docs/api/props-image) | `originUrl`, `maskUrl`, `initialSession`, `initialPaintColor` |
| [Semantic Colors & Outline](/docs/api/props-semantic) | `semanticColors`, `regionOutlineColor` |
| [maskConfig](/docs/api/mask-config) | Segmentation and semantic region configuration |
| [pipelineConfig](/docs/api/pipeline-config) | Resolution and processing pipeline configuration |
| [paintConfig](/docs/api/paint-config) | Paint rendering and texture blending configuration |
| [interactionConfig](/docs/api/interaction-config) | Touch interaction and hit testing configuration |
| [UI Controls & Styling](/docs/api/ui-controls) | Visibility toggles, custom renderers, styling |
| [Callbacks](/docs/api/callbacks) | `onWatch`, `onPaintCallback`, `onError` |
| [Ref Methods](/docs/api/ref-methods) | Imperative methods via `ref` |
| [Storage Convention](/docs/api/storage) | Session persistence and PNG export |

View File

@ -0,0 +1,15 @@
---
id: interaction-config
title: "Props: interactionConfig"
---
# 👆 Props: interactionConfig
| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `pickMapSearchRadiusPx` | `number` | `14` | Click pickMap search radius (pixels) |
| `kickMaskPickRadiusPx` | `number` | `36` | Baseboard mask pick radius |
| `thinStripPadding` | `number` | `0.008` | Thin strip (baseboard) tap expansion ratio |
| `regionPadding` | `number` | `0.003` | Normal region tap expansion ratio |
| `initRegionFlashMs` | `number` | `1000` | Duration each dashed outline stays during initial carousel (ms) |
| `enableInitRegionFlash` | `boolean` | `true` | Enable initial carousel animation |

View File

@ -0,0 +1,33 @@
---
id: mask-config
title: "Props: maskConfig"
---
# 🧩 Props: maskConfig
| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `semanticColors` | `MaskSemanticColor[]` | built-in palette | Mask semantic colors (overridable by top-level `semanticColors`) |
| `blackThreshold` | `number` | `30` | Pixels with max(B,G,R) below this value are treated as black background |
| `maxRegionColors` | `number` | `6` | Maximum semantic regions retained |
| `quantStep` | `number` | `64` | Baseboard quantization step |
| `baseboardMaxColorDist` | `number` | `42` | Baseboard color distance threshold |
| `baseboardStripQuantKeys` | `string[]` | built-in keys | Baseboard strip quantization keys, format `"b,g,r"` |
| `wallQuantKeys` | `string[]` | built-in keys | Wall quantization keys |
| `cabinetQuantKeys` | `string[]` | built-in keys | Cabinet quantization keys |
| `secondarySemanticNames` | `string[]` | `garageDoor, roof, eave` | Secondary semantic names |
| `secondaryMinPixelRatio` | `number` | `0.002` | Minimum pixel ratio for secondary semantics |
| `junctionHRadiusPx` | `number` | `24` | Baseboard junction horizontal radius |
| `junctionVRadiusPx` | `number` | `2` | Baseboard junction vertical radius |
| `kickBridgeHalfWPx` | `number` | `6` | Baseboard horizontal gap bridge half-width |
| `baseboardJunctionRowMarginPx` | `number` | `1` | Baseboard junction row margin |
| `baseboardJunctionVReachPx` | `number` | `2` | Baseboard junction vertical reach |
| `baseboardMinRunPx` | `number` | `2` | Minimum run length for mask strips |
| `splitWalls` | `boolean` | `false` | Split wall mask into `wall-1`, `wall-2`… by texture boundaries |
| `splitWallsMaxCount` | `number` | `8` | Max wall sub-region count |
| `splitWallsMinAreaRatio` | `number` | `0.002` | Minimum area ratio for fragments (relative to total seg pixels) |
| `splitWallsColorDistSq` | `number` | `1400` | Connected-component chroma mean distance squared threshold |
| `splitWallsChromaBlurRadius` | `number` | `5` | Reserved: chroma smoothing radius |
| `splitWallsNeutralChromaMax` | `number` | `14` | White/gray wall low-chroma radius; forced boundary from colored walls |
When `splitWalls` is enabled, the single `wall` region is replaced by multiple `wall-N` sub-regions, each independently paintable and undoable. Old sessions with `regionName: 'wall'` cannot map to new sub-region names and must be repainted.

View File

@ -0,0 +1,21 @@
---
id: paint-config
title: "Props: paintConfig"
---
# 🖌️ Props: paintConfig
| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `palette` | `BgrColor[]` | 6-color built-in | Bottom brush color strip |
| `colorBaseOpacity` | `number` | `0.88` | Base color opacity |
| `lLightOpacity` | `number` | `0.50` | L-channel overlay intensity |
| `textureOpacity` | `number` | `0.85` | High-frequency texture overlay intensity (stronger texture preservation) |
| `lLowBlurKernel` | `number` | `7` | Low-frequency Gaussian kernel (odd number) |
| `lLowContrast` | `number` | `1.15` | Low-frequency contrast |
| `lLowBrightness` | `number` | `0.9` | Low-frequency brightness |
| `lHighGain` | `number` | `1.22` | High-frequency gain |
| `maskFeatherColor` | `number` | `1.6` | Paint edge feathering (color) — soft-edge alpha radius, in pixels |
| `maskFeatherTexture` | `number` | `0.9` | Paint edge feathering (texture) — reserved/auxiliary |
| `regionOverlayFill` | `string` | `rgba(20,120,235,0.58)` | Dashed line / highlight fill color |
| `regionOutlineStrokeWidth` | `number` | `4` | Dashed outline stroke width |

View File

@ -0,0 +1,16 @@
---
id: pipeline-config
title: "Props: pipelineConfig"
---
# 🔬 Props: pipelineConfig
| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `maxImageLongSide` | `number` | `720` | Maximum long side for segmentation / pickMap / working area scaling |
| `paintFreqMaxLongSide` | `number` | `480` | Maximum long side for OpenCV LAB frequency layers |
| `originPreviewMaxLongSide` | `number` | `360` | Maximum long side for preview (main path uses working resolution) |
| `maskPathMaxLongSide` | `number` | `480` | Maximum long side for outline contour downsampling |
| `minContourArea` | `number` | `100` | Minimum contour area (scales proportionally with resolution) |
| `contourApproxEpsilon` | `number` | `0.003` | Contour polygon approximation coefficient |
| `maxRegions` | `number` | `500` | Maximum region count during segmentation |

View File

@ -0,0 +1,18 @@
---
id: props-image
title: "Props: Image & Initialization"
---
# 🖼️ Props: Image & Initialization
| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `originUrl` | `string` | yes* | — | Origin image URL (`file://`, absolute path, or `http(s)://`) |
| `maskUrl` | `string` | yes* | — | Mask image URL (semantic color-block image; recommended same dimensions as origin) |
| `originImgPath` | `string` | — | — | **Deprecated** — use `originUrl` |
| `maskImgPath` | `string` | — | — | **Deprecated** — use `maskUrl` |
| `initialSession` | `MaskSegmentSession` | no | — | Draft restored from MMKV etc.; automatically calls `loadSession` after regions are ready |
| `initialPaintColor` | `BgrColor` | no | — | **Optional**. Initial custom brush color `{ b, g, r }`; if omitted, no brush is selected by default; user must select a color or call `ref.setPaintColor` |
| `initialPaintConfigJson` | `Record<string, unknown>` | no | — | **Optional**. Accompanying brush config for `initialPaintColor`; passed back via `onPaintCallback` on successful paint |
\* At least one of `originUrl` / `maskUrl` is required per usage context.

View File

@ -0,0 +1,25 @@
---
id: props-semantic
title: "Props: Semantic Colors & Outline"
---
# 🎨 Props: Semantic Colors & Outline
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `semanticColors` | `MaskSemanticColor[]` | `MASK_SEMANTIC_COLORS` | Mask semantic recognition colors; equivalent to `maskConfig.semanticColors` |
| `regionOutlineColor` | `string` | `rgba(20, 120, 235, 0.58)` | Region dashed highlight color; equivalent to `paintConfig.regionOverlayFill` |
Top-level props take priority over nested `maskConfig` / `paintConfig`.
## MaskSemanticColor Structure
```ts
{
name: string; // Semantic name, e.g. wall / ceiling / baseboard
hex: string; // Display hex color
bgr: { b: number; g: number; r: number }; // Must match mask pixel BGR channels
}
```
Built-in palette: `MASK_SEMANTIC_COLORS` (see `src/utils/maskSemanticPalette.ts`).

View File

@ -0,0 +1,52 @@
---
id: ref-methods
title: "Ref Methods"
---
# 🔧 Ref Methods
Accessed via `ref` (type `MaskSegmentCanvasRef`):
| Method | Signature | Description |
| --- | --- | --- |
| `reset` | `() => void` | Undo last paint step (by `paintHistory`) |
| `swap` | `(showOrigin?: boolean) => void` | Toggle origin image comparison; omit arg to toggle, `true`/`false` to force |
| `save` | `(options?) => Promise<SavePaintResult>` | Composite and save PNG; `options.destDir` optional output directory |
| `session` | `() => MaskSegmentSession` | Export JSON-serializable session (for MMKV storage) |
| `loadSession` | `(session) => void` | Restore paint state (also available via `initialSession`) |
| `setPaintColor` | `(color, configJson?) => void` | Set current brush color; clears bottom color bar selection |
| `setMaskConfig` | `(config) => void` | Update mask config at runtime and **re-segment** |
| `clearAllPaint` | `() => void` | Clear all paint records |
| `resegment` | `() => Promise<void>` | Clear PNG cache and re-segment |
| `getRegions` | `() => SegmentRegion[]` | Snapshot of current region list |
| `getPaintedRegions` | `() => PaintedRegionRecord[]` | Snapshot of current paint records |
## SavePaintResult
`{ filePath, width, height, paintedCount, previewPath? }`
## Code Examples
```tsx
const ref = useRef<MaskSegmentCanvasRef>(null);
ref.current?.reset();
ref.current?.swap(); // toggle
ref.current?.swap(true); // force show origin
const result = await ref.current?.save({ destDir: '/path/to/dir' });
const session = ref.current?.session();
ref.current?.loadSession(session);
ref.current?.setPaintColor({ b: 100, g: 120, r: 140 }, { sku: 'paint-001' });
ref.current?.setMaskConfig({ semanticColors: customColors });
ref.current?.clearAllPaint();
await ref.current?.resegment();
const regions = ref.current?.getRegions();
const painted = ref.current?.getPaintedRegions();
```
> `save` depends on the working buffer and pickMap being ready (typically after `interactive`); throws `'Image not ready, cannot save'` if not ready.

26
docs/docs/api/storage.md Normal file
View File

@ -0,0 +1,26 @@
---
id: storage
title: "Storage Convention"
---
# 💾 Storage Convention
| Capability | Recommended Storage | Content |
| --- | --- | --- |
| `ref.save()` | File system | Full-res PNG path |
| `ref.session()` | MMKV / AsyncStorage | JSON metadata (URLs, paint records, brush color, etc.) |
## MaskSegmentSession Structure
```ts
{
version: 1;
originUrl: string;
maskUrl: string;
painted: PaintedRegionRecord[]; // { regionId, regionName, color, configJson? }
paintHistory: number[];
currentColor?: BgrColor;
currentColorConfigJson?: Record<string, unknown>;
savedAt: number;
}
```

View File

@ -0,0 +1,24 @@
---
id: ui-controls
title: "Props: UI Controls & Styling"
---
# 🎛️ Props: UI Controls & Styling
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `showToolbar` | `boolean` | `true` | Top toolbar ("Clear cache & re-segment") |
| `showColorBar` | `boolean` | `true` | Bottom brush color strip |
| `showStatusRow` | `boolean` | `true` | Segmentation/loading status text |
| `showOverlayButtons` | `boolean` | `true` | Bottom-left undo, bottom-right compare buttons |
| `showDebugPickers` | `boolean` | `true` | Photo library debug picker (set to `false` in production) |
| `disabled` | `boolean` | `false` | Disable paint interaction |
| `style` | `ViewStyle` | — | Outer container style |
| `canvasStyle` | `ViewStyle` | — | Canvas area style |
| `undoButtonStyle` / `compareButtonStyle` | `ViewStyle` | — | Overlay button styles |
| `undoButtonTextStyle` / `compareButtonTextStyle` | `TextStyle` | — | Overlay button text styles |
| `undoButtonText` | `string` | `Undo` | Undo button label |
| `compareButtonText` | `string` | `Compare` | Enter compare mode label |
| `compareExitButtonText` | `string` | `Exit Compare` | Exit compare mode label |
| `renderUndoButton` | `(props) => ReactNode` | — | Custom undo button renderer |
| `renderCompareButton` | `(props) => ReactNode` | — | Custom compare button renderer |

203
docs/docs/basic-usage.md Normal file
View File

@ -0,0 +1,203 @@
---
id: basic-usage
title: Basic Usage
---
# 💡 Basic Usage
## 🧑‍💻 Minimal Example
A complete, copy-pasteable example covering **PNG pre-warming**, **state management**, **configuration**, **loading states**, **onWatch**, and **ref operations**.
```tsx
import React, { useEffect, useRef, useState } from 'react';
import { ActivityIndicator, Text, View } from 'react-native';
import MaskSegmentCanvas, {
type MaskSegmentCanvasRef,
type MaskSegmentSession,
type MaskSegmentWatchState,
type BgrColor,
MASK_SEMANTIC_COLORS,
prewarmPngBgrCacheAsync,
} from 'react-native-mask-segment-canvas';
/** Image paths prepared by the host app (local file:// or http(s)://) */
type ImagePaths = {
origin: string;
mask: string;
};
const INTERACTIVE_STATES: MaskSegmentWatchState[] = [
'interactive',
'mask_paths_ready',
];
export function PaintScreen() {
const canvasRef = useRef<MaskSegmentCanvasRef>(null);
const [imagePaths, setImagePaths] = useState<ImagePaths | null>(null);
const [pathsError, setPathsError] = useState('');
const [watchState, setWatchState] = useState<MaskSegmentWatchState | ''>('');
const [errorMessage, setErrorMessage] = useState('');
const [sessionDraft] = useState<MaskSegmentSession | null>(null);
const isInteractive = INTERACTIVE_STATES.includes(
watchState as MaskSegmentWatchState,
);
const isOutlineReady = watchState === 'mask_paths_ready';
const isCanvasLoading =
imagePaths != null &&
watchState !== '' &&
!INTERACTIVE_STATES.includes(watchState as MaskSegmentWatchState) &&
watchState !== 'error';
useEffect(() => {
let cancelled = false;
(async () => {
try {
const origin = 'file:///path/to/origin.png';
const mask = 'file:///path/to/mask.png';
await prewarmPngBgrCacheAsync([origin, mask]);
if (!cancelled) {
setImagePaths({ origin, mask });
}
} catch (e) {
if (!cancelled) {
setPathsError(e instanceof Error ? e.message : String(e));
}
}
})();
return () => { cancelled = true; };
}, []);
const handleSave = async () => {
if (!isInteractive) return;
const result = await canvasRef.current?.save({ destDir: undefined });
console.log('saved', result?.filePath, result?.paintedCount);
};
if (pathsError) {
return <Text>{pathsError}</Text>;
}
if (!imagePaths) {
return (
<View>
<ActivityIndicator />
<Text>Waiting for origin and mask images...</Text>
</View>
);
}
return (
<View style={{ flex: 1 }}>
{isCanvasLoading ? <Text>Loading: {watchState}</Text> : null}
{watchState === 'interactive' ? (
<Text>Paintable (outlines loading...)</Text>
) : null}
{isOutlineReady ? <Text>Ready</Text> : null}
{errorMessage ? <Text>{errorMessage}</Text> : null}
<MaskSegmentCanvas
ref={canvasRef}
style={{ flex: 1 }}
originUrl={imagePaths.origin}
maskUrl={imagePaths.mask}
semanticColors={MASK_SEMANTIC_COLORS}
regionOutlineColor="rgba(20, 120, 235, 0.58)"
maskConfig={{ blackThreshold: 30, maxRegionColors: 6 }}
pipelineConfig={{ maxImageLongSide: 720 }}
paintConfig={{ colorBaseOpacity: 0.88 }}
interactionConfig={{
initRegionFlashMs: 1000,
enableInitRegionFlash: true,
}}
initialSession={sessionDraft ?? undefined}
showDebugPickers={false}
showToolbar={false}
showColorBar
showStatusRow={false}
showOverlayButtons
disabled={!isInteractive}
onWatch={(state, durationMs, detail) => {
setWatchState(state);
console.log('[onWatch]', state, durationMs, detail);
}}
onPaintCallback={payload => {
if (payload.kind === 'brush_required') {
toast(payload.hint);
return;
}
console.log('painted', payload.regionId, payload.regionName, payload.color, payload.configJson);
}}
onError={message => {
setErrorMessage(message);
setWatchState('error');
}}
/>
</View>
);
}
```
---
## 📊 State Variables
| State | Type | Purpose |
| --- | --- | --- |
| `imagePaths` | `{ origin, mask } \| null` | Local/remote image paths resolved by the host |
| `pathsError` | `string` | Error message when path resolution or PNG pre-warming fails |
| `watchState` | `MaskSegmentWatchState \| ''` | Initialization stage reported by `onWatch` |
| `isInteractive` | derived | `true` when `interactive` or `mask_paths_ready` — operations are allowed |
| `isOutlineReady` | derived | `true` when `mask_paths_ready` — carousel dashed outlines are ready |
| `isCanvasLoading` | derived | Canvas init is blocking (not including PNG path waiting) |
| `errorMessage` | `string` | Segmentation/loading failure message written by `onError` |
| `sessionDraft` | `MaskSegmentSession \| null` | Draft restored from MMKV or similar storage |
---
## ⚙️ Choosing Configuration Values
| Config | Use top-level prop when... | Use nested Config when... |
| --- | --- | --- |
| Semantic colors | `semanticColors={...}` for most cases | `maskConfig.semanticColors` when paired with other mask params |
| Outline color | `regionOutlineColor="..."` for most cases | `paintConfig.regionOverlayFill` when also customizing the brush palette |
| Black threshold, max regions | — | `maskConfig` |
| Image processing size | — | `pipelineConfig` |
| Flash interval, tap tolerance | — | `interactionConfig` |
Top-level props and nested Configs **can coexist**; top-level `semanticColors` / `regionOutlineColor` take priority.
---
## 🔄 watchState & UI Guidance
```ts
// Blocking loading (before regions + paint layers are ready)
const isLoading = ![
'interactive',
'mask_paths_ready',
'error',
'',
].includes(watchState);
// Allow tapping regions, selecting colors, painting
const canOperate =
watchState === 'interactive' || watchState === 'mask_paths_ready';
// Carousel dashed outlines are fully ready
const isOutlineReady = watchState === 'mask_paths_ready';
// Show error screen
const hasError = watchState === 'error';
```
At `interactive`, `detail.maskPathsReady` is typically `false`; at `mask_paths_ready`, it is `true`. The gap is roughly ~100ms (async Skia path construction) and does not block tap-to-paint.
`originUrl` / `maskUrl` support:
- Local paths: `file:///...` or absolute paths
- Remote URLs: `http(s)://...` (the component handles download and decoding internally)
> Legacy props `originImgPath` / `maskImgPath` are deprecated; use `originUrl` / `maskUrl` instead.

17
docs/docs/dependencies.md Normal file
View File

@ -0,0 +1,17 @@
---
id: dependencies
title: Dependencies
---
# 📚 Dependencies
| Package | Purpose |
| --- | --- |
| `@shopify/react-native-skia` | Canvas rendering, Path, dashed strokes, Blend compositing |
| `react-native-fast-opencv` | Mask morphology, contour processing |
| `react-native-fs` | Layer caching, PNG save |
| `react-native-image-picker` | Demo photo library picker (optional) |
| `react-native-reanimated` | Skia animation dependency |
| `react-native-safe-area-context` | Safe area insets (optional) |
| `buffer` | Binary buffer polyfill |
| `upng-js` | PNG encoding/decoding in pure JS |

View File

@ -0,0 +1,25 @@
---
id: custom-colors
title: Custom Semantic Color Table
---
# 🎨 Custom Semantic Color Table
Define custom semantic colors when your mask uses different color values:
```tsx
const gymColors: MaskSemanticColor[] = [
{ name: 'wall', hex: '#4363D8', bgr: { b: 216, g: 99, r: 67 } },
{ name: 'ceiling', hex: '#3CB44B', bgr: { b: 75, g: 180, r: 60 } },
// ...
];
<MaskSegmentCanvas
semanticColors={gymColors}
maskConfig={{ blackThreshold: 30, maxRegionColors: 6 }}
/>
```
:::caution
`semanticColors` must match the semantic colors used in the backend/labeled mask; mismatch will cause recognition drift.
:::

View File

@ -0,0 +1,18 @@
---
id: draft-recovery
title: Draft Recovery
---
# 💾 Draft Recovery
```tsx
const draft = JSON.parse(mmkv.getString('paint_draft'));
<MaskSegmentCanvas
originUrl={draft.originUrl}
maskUrl={draft.maskUrl}
initialSession={draft}
/>
```
Use `ref.session()` to export the current session and store it in MMKV or AsyncStorage for later recovery.

View File

@ -0,0 +1,21 @@
---
id: local-paths
title: Passing Local Paths from an API
---
# 🌐 Passing Local Paths from an API
```tsx
<MaskSegmentCanvas
originUrl={localOriginPath}
maskUrl={localMaskPath}
showDebugPickers={false}
showToolbar={false}
semanticColors={MASK_SEMANTIC_COLORS}
regionOutlineColor="#1e96ff"
onWatch={(state, ms, detail) => {
if (state === 'interactive') hideBlockingLoader();
if (state === 'mask_paths_ready') hideOutlineHint();
}}
/>
```

View File

@ -0,0 +1,19 @@
---
id: png-pre-warm
title: Pre-warm PNG Cache (Recommended)
---
# 🔥 Pre-warm PNG Cache (Recommended)
Pre-warm the PNG decode cache before mounting the canvas to save **100250ms** on initialization.
```tsx
import { prewarmPngBgrCacheAsync } from 'react-native-mask-segment-canvas';
async function openPaintScreen(originUrl: string, maskUrl: string) {
await prewarmPngBgrCacheAsync([originUrl, maskUrl]);
navigation.navigate('Paint', { originUrl, maskUrl });
}
```
Call `prewarmPngBgrCacheAsync` after download/extraction and before navigating to the paint screen for the best performance.

136
docs/docs/installation.md Normal file
View File

@ -0,0 +1,136 @@
---
id: installation
title: Installation
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
# 📦 Installation
## Peer Dependencies
Install these in your host project (versions should match your host RN version):
```bash
npm install @shopify/react-native-skia react-native-reanimated react-native-fast-opencv react-native-fs buffer upng-js react-native-gesture-handler
# If using showDebugPickers (photo library picker)
npm install react-native-image-picker
# Safe area insets
npm install react-native-safe-area-context
```
## Postinstall Setup
This library relies on `patch-package` to patch `react-native-fast-opencv`. Your host `package.json` must include:
```json
{
"scripts": {
"postinstall": "patch-package"
},
"devDependencies": {
"patch-package": "^8.0.1"
}
}
```
After installing this library, patches from `node_modules/react-native-mask-segment-canvas/patches/` are applied automatically during the host's `postinstall`.
## iOS / Android Native Dependencies
```bash
cd ios && pod install && cd ..
```
Ensure the host project has completed Skia, Reanimated, and OpenCV native setup per each library's documentation.
## Metro Configuration
When using `npm link`, a monorepo, or `file:` dependencies, add this library to `watchFolders` and use `extraNodeModules` + `blockList` to prevent duplicate module resolution:
```js
const path = require('path');
module.exports = {
watchFolders: [path.resolve(__dirname, '../MaskSegmentApp')],
resolver: {
nodeModulesPaths: [path.resolve(__dirname, 'node_modules')],
extraNodeModules: {
'react-native-reanimated': path.resolve(__dirname, 'node_modules/react-native-reanimated'),
'@shopify/react-native-skia': path.resolve(__dirname, 'node_modules/@shopify/react-native-skia'),
'react-native-gesture-handler': path.resolve(__dirname, 'node_modules/react-native-gesture-handler'),
'react-native-fast-opencv': path.resolve(__dirname, 'node_modules/react-native-fast-opencv'),
'react-native-safe-area-context': path.resolve(__dirname, 'node_modules/react-native-safe-area-context'),
'react-native-fs': path.resolve(__dirname, 'node_modules/react-native-fs'),
},
blockList: [
/\/MaskSegmentApp\/node_modules\/@shopify\/react-native-skia\//,
/\/MaskSegmentApp\/node_modules\/react-native-reanimated\//,
/\/MaskSegmentApp\/node_modules\/react-native-fast-opencv\//,
/\/MaskSegmentApp\/node_modules\/react-native-gesture-handler\//,
/\/MaskSegmentApp\/node_modules\/react-native-safe-area-context\//,
/\/MaskSegmentApp\/node_modules\/react-native-fs\//,
],
},
};
```
**Strongly recommended** — add this at the very top of the host `index.js` (before any business code):
```js
import '@shopify/react-native-skia';
```
See `example/metro.config.js` and `example/index.js` for the complete configuration with all peer singleton packages.
## Troubleshooting: Duplicate Module Errors
Common symptoms:
- `SkiaPictureView must be a function (received 'undefined')`
- `createAnimatedNode: Animated node[...] already exists`
These are almost always caused by Metro resolving multiple copies of reanimated / skia / gesture-handler / fast-opencv / safe-area packages.
**Best practice:**
1. Copy the `singletonPackages` + `extraNodeModules` + `blockList` pattern from `example/metro.config.js`
2. At the top of your `index.js`, import gesture-handler → reanimated → skia in order
3. Restart Metro with `--reset-cache` and reinstall the app
See the example project for a detailed checklist and template.
### Integration Methods
<Tabs>
<TabItem value="npm" label="npm install (Production)">
```bash
npm install react-native-mask-segment-canvas
```
</TabItem>
<TabItem value="link" label="npm link (Development)">
```bash
# In the library directory
npm link
# In your project
npm link react-native-mask-segment-canvas
```
</TabItem>
<TabItem value="file" label="file: dependency">
```json
{
"dependencies": {
"react-native-mask-segment-canvas": "file:../MaskSegmentApp"
}
}
```
</TabItem>
</Tabs>

View File

@ -0,0 +1,13 @@
---
id: interaction-guide
title: Interaction Guide
---
# 🎮 Interaction Guide
1. 🔁 **Initial Carousel**: After regions are ready, each region's dashed outline flashes sequentially per `initRegionFlashMs` (default 1s); stops on first user touch.
2. 🔍 **Preview (no brush selected)**: Long-press a region to show dashed outline for the connected component under the touch point; tapping a black area shows no outline.
3. 🎨 **Paint (brush selected)**: Tap a color in the bottom color bar or call `ref.setPaintColor` (or preselect via `initialPaintColor`), then tap a region to paint; tapping the same region again overwrites the color.
4. 💬 **Tap without brush**: No paint is performed; `onPaintCallback` fires with `kind: 'brush_required'`, carrying a `hint` and target region info for the host to show a toast/modal prompting color selection.
5. ↩️ **Undo**: Bottom-left button or `ref.reset()`; steps backward through paint history one action at a time.
6. 👁️ **Compare with Origin**: Bottom-right button or `ref.swap()`; hides the paint layer to show the original image.

54
docs/docs/intro.md Normal file
View File

@ -0,0 +1,54 @@
---
id: intro
title: Overview
sidebar_position: 1
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
# 🎨 react-native-mask-segment-canvas
A React Native **0.79** interactive mask segmentation library. The core export is the `MaskSegmentCanvas` component, consumable via **npm package** or **npm link** from any React Native project.
- 🧠 **OpenCV** (`react-native-fast-opencv`): mask semantic layout, baseboard patching, region extraction
- 🖌️ **Skia RuntimeEffect (SkSL)**: single-pass full-screen shader blending original image + LAB low/high frequency texture color overlays
- ✂️ **Skia Path**: dashed outline highlights for regions
- 👆 **Interaction**: bottom color bar for brush selection (optional initialization) → tap a region to paint; tapping without a brush selected fires `onPaintCallback` with a hint; long-press without a brush previews the region's dashed outline
This repository serves as both the **library source** (`src/index.ts`) and a **self-test demo** (root `App.tsx`).
📌 **For the recommended integration demo, see the `example/` directory** — it uses only the public API, fully simulating how a consumer project would integrate (including `package.json`, Metro configuration, and a complete reference `App.tsx`).
---
## 🔭 Overview
`MaskSegmentCanvas` renders an original image with an overlaid semantic mask, allowing users to tap regions and apply colors. The pipeline:
1. 📥 **Load** the origin image and mask image (local `file://` or remote `http(s)://`)
2. 🧩 **Segment** the mask via OpenCV into semantic regions (walls, ceiling, baseboard, etc.)
3. 🎨 **Prepare** LAB frequency-layer textures via SkSL for realistic color blending
4. 📐 **Build** Skia dashed-outline paths for each region
5. 👆 **Interactive** — users select a brush color and tap regions to paint; paint layers preserve the underlying texture
6. 💾 **Save** the composited result as PNG; export a JSON session for draft recovery
The component emits `onWatch` state transitions through the pipeline so the host app can show appropriate loading states.
---
## 📋 Requirements
- 🟢 Node.js >= 18 (recommended 20+)
- 🍎 Xcode 15+ (iOS)
- 🤖 Android Studio + JDK 17 (Android)
- 📦 CocoaPods (iOS)
---
## 🚀 Next Steps
- **[Installation](/docs/installation)** — set up the library in your project
- **[Quick Start](/docs/quick-start)** — run the dev demo
- **[Basic Usage](/docs/basic-usage)** — minimal example to get started
- **[API Reference](/docs/api)** — full prop and method documentation

11
docs/docs/notes.md Normal file
View File

@ -0,0 +1,11 @@
---
id: notes
title: Notes
---
# 📝 Notes
- The mask image should be a semantic color-block image with the same dimensions as the origin (black background + solid-color regions). Pixels with `max(B,G,R) < blackThreshold` (default 30) are excluded from segmentation.
- OpenCV segmentation runs on the JS thread; very large images may cause frame drops. Use `pipelineConfig.maxImageLongSide` to cap processing resolution.
- iOS photo library access requires photo permissions (only needed when `showDebugPickers` is enabled).
- `semanticColors` must match the semantic colors used in the backend/labeled mask; mismatch will cause recognition drift.

92
docs/docs/performance.md Normal file
View File

@ -0,0 +1,92 @@
---
id: performance
title: Performance
---
# ⚡ Performance
The data below is based on the Demo test image (`assets/test/origin.png` **1080×1920**, 6 semantic regions), **default `pipelineConfig`**, and `onWatch` `durationMs` (measured from `init`). These are **empirical ranges**, not strict benchmarks; actual device results vary with CPU, storage, and RN version.
## Measured Reference (Dev Env + PNG Pre-warming)
The Demo calls `prewarmPngBgrCacheAsync([origin, mask])` before mounting the canvas, so PNG decoding hits the memory cache. Typical logs:
| Stage | watchState | Approx. Duration | Notes |
| --- | --- | --- | --- |
| Mask aligned | `mask_aligned` | ~160ms | Mask scaled to segmentation working resolution |
| Regions ready | `regions_ready` / `mask_sampled` | ~320ms | Layout scan + baseboard + pickMap |
| **Interactive** | `**interactive`** | **~320450ms** | Can tap regions, select colors, Shader paint |
| Outlines ready | `mask_paths_ready` | ~430550ms | ~100ms after `interactive`; carousel outlines can display |
`interactive` does **not wait** for outline paths; `mask_paths_ready` only affects the initial carousel and optional UI hints.
Same-image sub-step magnitudes (`__DEV__` logs, default pipeline):
| Sub-step | Approx. Duration | Working Resolution |
| --- | --- | --- |
| OpenCV LAB high/low freq | ~1040ms | 270×480 |
| High/low freq Skia textures | ~2030ms | same |
| Layout scan + baseboard + pick table | ~90120ms | 405×720 (1080p → longSide 720) |
| Full contour paths (async, non-blocking) | ~80150ms | 270×480 |
## Resolution vs pipelineConfig
Compute-intensive steps are capped by **maximum long side limits** and do **not scale linearly with 4K/8K origin images**. **Full PNG decoding** still scales linearly with pixel count.
| Step | Config Key | 1080×1920 Actual Size | Scales with Origin Pixels |
| --- | --- | --- | --- |
| PNG decode | — | 1080×1920 × 2 images | **Yes** |
| Mask seg / pickMap | `maxImageLongSide: 720` | ~405×720 | **No** (fixed when long side >720) |
| Shader high/low freq | `paintFreqMaxLongSide: 480` | ~270×480 | **No** |
| Working area Skia origin | same as `maxImageLongSide` | ~405×720 | **No** |
| Dashed outlines | `maskPathMaxLongSide: 480` | ~270×480 | **No** (does not block `interactive`) |
## interactive Estimation (Default Pipeline)
| Origin Spec | Relative to 1080p Pixels | With PNG Pre-warm | Cold Start (no pre-warm) |
| --- | --- | --- | --- |
| 1080×1920 | 1× | **320450ms** | **450700ms** |
| 1440×2560 (2K) | ~1.8× | **400550ms** | **600900ms** |
| 3840×2160 (4K) | ~4× | **500750ms** | **8001200ms** |
| 7680×4320 (8K) | ~16× | **0.81.5s** | **1.53s+** |
> **`<300ms` interactive**: achievable on 1080p + pre-warm + default pipeline + high-end devices, but **optimistic** — do not treat as an all-device SLA.
## Device Tier (1080p, Default Pipeline)
Relative to the ~320ms dev-environment baseline:
| Tier | Relative Multiplier | Pre-warm `interactive` | Cold Start |
| --- | --- | --- | --- |
| Flagship iOS / new flagship Android | 0.81.2× | 300450ms | 500800ms |
| Mid-range Android | 1.52.5× | 500800ms | 700ms1.2s |
| Low-end Android (4GB, old SoC) | 2.54× | 800ms1.3s | 12s+ |
Android overhead primarily comes from: JS ↔ OpenCV bridge, memory bandwidth/GC, Skia texture upload.
## Impact of Raising maxImageLongSide
Setting `pipelineConfig.maxImageLongSide` to **1280** (above the default 720) results in a segmentation working area of ~720×1280, roughly **3×** the pixel count of the 720 tier:
| Scenario | Default 720 | Raised to 1280 |
| --- | --- | --- |
| 1080p `interactive` (mid-range) | ~320800ms | **500ms1s+** |
| Segmentation / pickMap duration | ~90120ms | ~250350ms |
Higher precision for longer init time. To stay **`<500ms` interactive**, keep the default **720**; reduce to **640** if needed.
## Optimization Tips
1. 🚀 **PNG pre-warming (recommended)**: Call `prewarmPngBgrCacheAsync` after download/extraction and before navigating to the paint screen. Typically saves **100250ms** (greatest benefit on low-end devices).
```tsx
import { prewarmPngBgrCacheAsync } from 'react-native-mask-segment-canvas';
await prewarmPngBgrCacheAsync([originPath, maskPath]);
// Then mount MaskSegmentCanvas
```
2. ⏱️ **Loading timing**: Dismiss the blocking loader at `interactive`; optionally listen for `mask_paths_ready` for "outlines preparing" hints.
3. 🖼️ **Large images / low-end devices**: Keep default `maxImageLongSide: 720`; optionally lower `paintFreqMaxLongSide` to **360**.
4. 📷 **4K assets**: Downsample on the host side before passing in, or accept ~0.81.5s `interactive` (with pre-warm).
5. 🔍 **Observability**: Watch Metro logs for `[MaskSegment]`, `[⏱ ...]` prefixes and `onWatch` `durationMs`.

View File

@ -0,0 +1,30 @@
---
id: project-structure
title: Project Structure
---
# 📁 Project Structure
```
MaskSegmentApp/ # Repo root (npm package react-native-mask-segment-canvas)
├── App.tsx # Dev self-test Demo (imports from ./src directly)
├── src/
│ ├── index.ts # Package entry (consumer: import 'react-native-mask-segment-canvas')
│ ├── components/
│ │ ├── MaskSegmentCanvas.tsx
│ │ └── MaskSegmentCanvas.types.ts
│ └── utils/
│ ├── maskSegmentation.ts
│ ├── maskSegmentRuntime.ts
│ ├── maskSemanticPalette.ts
│ └── ...
├── example/ # ★ Recommended: consumer-side integration demo
│ ├── App.tsx # Full example using only the public API
│ ├── index.js / app.json
│ ├── package.json # Required deps + "react-native-mask-segment-canvas": "file:.."
│ ├── metro.config.js / babel.config.js / tsconfig.json
│ └── README.md # How to integrate in a real project
├── patches/ # Shipped with the package; applied by host postinstall
├── ios/ # Root Demo native project (not published to npm)
└── android/
```

25
docs/docs/quick-start.md Normal file
View File

@ -0,0 +1,25 @@
---
id: quick-start
title: Quick Start (Dev Demo)
---
# 🚀 Quick Start (Dev Demo)
The root `App.tsx` is a full self-test demo that imports directly from `./src`.
```bash
cd MaskSegmentApp
npm install
cd ios && bundle exec pod install && cd ..
npm start
# In another terminal
npm run ios
# or
npm run android
```
**To see how a consumer project integrates:** go to the `example/` directory and follow its `README.md`. It uses `import from 'react-native-mask-segment-canvas'` with standard `package.json` and Metro config, fully simulating a consumer environment.

View File

@ -0,0 +1,42 @@
---
id: troubleshooting
title: Troubleshooting
---
# 🔧 Troubleshooting
## iOS pod install fails
```bash
cd ios
bundle install
bundle exec pod install --repo-update
```
## Android build errors
```bash
cd android && ./gradlew clean && cd ..
```
## Segmentation fails / zero regions
- Verify `originUrl` / `maskUrl` are accessible
- Confirm mask semantic colors match the `semanticColors` config
- Check Metro logs for `[MaskSegment]` / `[⏱ ...]` output
## Dashed outlines misaligned / extra contours
- Outlines are generated from mask pixel external contours; long-press only shows the connected component at the touch point
- The initial carousel only shows the largest connected component for each semantic region
## Common Duplicate Module Errors
**Symptoms:**
- `SkiaPictureView must be a function (received 'undefined')`
- `createAnimatedNode: Animated node[...] already exists`
**Solution:**
1. Copy the `singletonPackages` + `extraNodeModules` + `blockList` pattern from `example/metro.config.js`
2. At the top of your `index.js`, import gesture-handler → reanimated → skia in order
3. Restart Metro with `--reset-cache` and reinstall the app

121
docs/docusaurus.config.ts Normal file
View File

@ -0,0 +1,121 @@
import { themes as prismThemes } from 'prism-react-renderer';
import type { Config } from '@docusaurus/types';
import type * as Preset from '@docusaurus/preset-classic';
const config: Config = {
title: 'MaskSegmentCanvas',
tagline: 'React Native interactive mask segmentation with OpenCV + Skia',
favicon: 'img/favicon.ico',
url: 'https://tonychan-hub.github.io',
baseUrl: '/react-native-mask-segment-canvas/',
organizationName: 'TonyChan-hub',
projectName: 'react-native-mask-segment-canvas',
onBrokenLinks: 'throw',
onBrokenMarkdownLinks: 'warn',
i18n: {
defaultLocale: 'en',
locales: ['en', 'zh-CN'],
localeConfigs: {
en: { label: 'English' },
'zh-CN': { label: '简体中文' },
},
},
presets: [
[
'classic',
{
docs: {
sidebarPath: './sidebars.ts',
editUrl: 'https://github.com/TonyChan-hub/react-native-mask-segment-canvas/tree/main/docs/',
},
theme: {
customCss: './src/css/custom.css',
},
} satisfies Preset.Options,
],
],
themeConfig: {
image: 'img/og-image.png',
navbar: {
title: 'MaskSegmentCanvas',
logo: {
alt: 'MaskSegmentCanvas Logo',
src: 'img/logo.svg',
},
items: [
{
type: 'docSidebar',
sidebarId: 'docsSidebar',
position: 'left',
label: 'Docs',
},
{
to: 'docs/api',
label: 'API',
position: 'left',
},
{
type: 'localeDropdown',
position: 'right',
},
{
href: 'https://github.com/TonyChan-hub/react-native-mask-segment-canvas',
label: 'GitHub',
position: 'right',
},
{
href: 'https://www.npmjs.com/package/react-native-mask-segment-canvas',
label: 'npm',
position: 'right',
},
],
},
footer: {
style: 'dark',
links: [
{
title: 'Docs',
items: [
{ label: 'Overview', to: 'docs/intro' },
{ label: 'Installation', to: 'docs/installation' },
{ label: 'Basic Usage', to: 'docs/basic-usage' },
{ label: 'API Reference', to: 'docs/api' },
],
},
{
title: 'Community',
items: [
{ label: 'GitHub', href: 'https://github.com/TonyChan-hub/react-native-mask-segment-canvas' },
{ label: 'npm', href: 'https://www.npmjs.com/package/react-native-mask-segment-canvas' },
],
},
{
title: 'More',
items: [
{ label: 'Performance', to: 'docs/performance' },
{ label: 'Troubleshooting', to: 'docs/troubleshooting' },
{ label: 'Example Project', to: 'docs/project-structure' },
],
},
],
copyright: `Copyright © ${new Date().getFullYear()} MaskSegmentCanvas. Built with Docusaurus.`,
},
prism: {
theme: prismThemes.github,
darkTheme: prismThemes.dracula,
additionalLanguages: ['bash', 'json', 'typescript'],
},
colorMode: {
defaultMode: 'dark',
disableSwitch: false,
respectPrefersColorScheme: true,
},
} satisfies Preset.ThemeConfig,
};
export default config;

View File

@ -0,0 +1,72 @@
---
id: callbacks
title: "Props回调"
---
# 📞 Props回调
| Prop | 签名 | 描述 |
| --- | --- | --- |
| `onWatch` | `(state, durationMs, detail?) => void` | 初始化阶段回调;`durationMs` 从本次 `init` 开始计时 |
| `onPaintCallback` | `(payload: PaintCallbackPayload) => void` | 成功上色时触发,或未选择画笔时点击区域触发 |
| `onError` | `(message, error?) => void` | 分割或加载失败 |
## PaintCallbackPayload
(可辨识联合类型,通过 `payload.kind` 区分):
```ts
// 成功上色
{
kind: 'painted';
regionId: number;
regionName: string;
color: BgrColor;
configJson?: Record<string, unknown>; // 来自 setPaintColor / initialPaintConfigJson
}
// 点击了有效区域但未选择画笔(未执行上色)
{
kind: 'brush_required';
hint: string; // 如 "请先选择画笔颜色(底部颜色条或 ref.setPaintColor"
regionId: number;
regionName: string;
}
```
示例:
```tsx
onPaintCallback={payload => {
if (payload.kind === 'brush_required') {
showToast(payload.hint);
return;
}
savePaintRecord(payload.regionId, payload.color, payload.configJson);
}}
```
## onWatch detailMaskSegmentWatchDetail
| 字段 | 类型 | 描述 |
| --- | --- | --- |
| `regionCount` | `number` | 当前有效区域数 |
| `maskPathsReady` | `boolean` | 轮廓 Skia 路径是否就绪 |
| `freqLayersReady` | `boolean` | 频域 Shader 纹理是否就绪 |
| `errorMessage` | `string` | `error` 状态下的失败描述 |
### onWatch 状态流转
```
init
→ images_loaded 原始图像和遮罩读取完成
→ mask_aligned 遮罩尺寸对齐
→ mask_sampled 遮罩像素采样完成
→ regions_ready 区域提取成功
→ layers_ready 上色纹理层就绪detail.maskPathsReady 可能仍为 false
→ interactive 可交互(可以点击区域、选择颜色、上色)
→ mask_paths_ready 轮廓路径就绪轮播虚线轮廓可显示detail.maskPathsReady 为 true
→ error 失败detail.errorMessage 包含描述)
```
`layers_ready` / `interactive` 可能在轮廓路径计算完成之前触发。如果宿主在 `interactive` 时关闭阻塞加载器,用户已可操作;轮播虚线轮廓在 `mask_paths_ready` 后自动显示。

View File

@ -0,0 +1,62 @@
---
id: index
title: API 参考
---
# 📖 API 参考
## 导入
```tsx
import MaskSegmentCanvas, {
type MaskSegmentCanvasRef,
type MaskSegmentCanvasProps,
type MaskSegmentSession,
type MaskSegmentWatchState,
type MaskSegmentWatchDetail,
type BgrColor,
type MaskSemanticColor,
type PaintCallbackPayload,
type PaintedRegionRecord,
type PipelineConfig,
type MaskSegmentConfig,
type PaintConfig,
type InteractionConfig,
type SavePaintResult,
MASK_SEMANTIC_COLORS,
BASEBOARD_SEMANTIC_NAME,
prewarmPngBgrCacheAsync,
DEFAULT_PIPELINE_CONFIG,
DEFAULT_MASK_CONFIG,
DEFAULT_PAINT_CONFIG,
DEFAULT_INTERACTION_CONFIG,
} from 'react-native-mask-segment-canvas';
```
| 分类 | 名称 |
| --- | --- |
| 组件 | `MaskSegmentCanvas`(默认导出) |
| Ref / Props 类型 | `MaskSegmentCanvasRef`, `MaskSegmentCanvasProps` |
| 会话 / 回调类型 | `MaskSegmentSession`, `PaintCallbackPayload`, `PaintedRegionRecord`, `SavePaintResult` |
| Watch 类型 | `MaskSegmentWatchState`, `MaskSegmentWatchDetail` |
| 配置类型 | `PipelineConfig`, `MaskSegmentConfig`, `PaintConfig`, `InteractionConfig` |
| 语义颜色 | `MASK_SEMANTIC_COLORS`, `BASEBOARD_SEMANTIC_NAME` |
| 工具函数 | `prewarmPngBgrCacheAsync` |
| 运行时默认值 | `DEFAULT_*_CONFIG` |
---
## Props 概览
| 分类 | 描述 |
| --- | --- |
| [图像与初始化](/docs/api/props-image) | `originUrl`, `maskUrl`, `initialSession`, `initialPaintColor` |
| [语义颜色与轮廓](/docs/api/props-semantic) | `semanticColors`, `regionOutlineColor` |
| [maskConfig](/docs/api/mask-config) | 分割和语义区域配置 |
| [pipelineConfig](/docs/api/pipeline-config) | 分辨率和处理管线配置 |
| [paintConfig](/docs/api/paint-config) | 上色渲染和纹理混合配置 |
| [interactionConfig](/docs/api/interaction-config) | 触摸交互和命中测试配置 |
| [UI 控件与样式](/docs/api/ui-controls) | 可见性开关、自定义渲染器、样式 |
| [回调](/docs/api/callbacks) | `onWatch`, `onPaintCallback`, `onError` |
| [Ref 方法](/docs/api/ref-methods) | 通过 `ref` 调用的命令式方法 |
| [存储约定](/docs/api/storage) | 会话持久化和 PNG 导出 |

View File

@ -0,0 +1,15 @@
---
id: interaction-config
title: "PropsinteractionConfig"
---
# 👆 PropsinteractionConfig
| 字段 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `pickMapSearchRadiusPx` | `number` | `14` | 点击 pickMap 搜索半径(像素) |
| `kickMaskPickRadiusPx` | `number` | `36` | 踢脚线遮罩拾取半径 |
| `thinStripPadding` | `number` | `0.008` | 细条(踢脚线)点击扩展比例 |
| `regionPadding` | `number` | `0.003` | 普通区域点击扩展比例 |
| `initRegionFlashMs` | `number` | `1000` | 初始轮播中每条虚线轮廓持续时长ms |
| `enableInitRegionFlash` | `boolean` | `true` | 启用初始轮播动画 |

View File

@ -0,0 +1,33 @@
---
id: mask-config
title: "PropsmaskConfig"
---
# 🧩 PropsmaskConfig
| 字段 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `semanticColors` | `MaskSemanticColor[]` | 内置调色板 | 遮罩语义颜色(可被顶层 `semanticColors` 覆盖) |
| `blackThreshold` | `number` | `30` | max(B,G,R) 低于此值的像素视为黑色背景 |
| `maxRegionColors` | `number` | `6` | 保留的最大语义区域数 |
| `quantStep` | `number` | `64` | 踢脚线量化步长 |
| `baseboardMaxColorDist` | `number` | `42` | 踢脚线颜色距离阈值 |
| `baseboardStripQuantKeys` | `string[]` | 内置键值 | 踢脚线索带量化键,格式 `"b,g,r"` |
| `wallQuantKeys` | `string[]` | 内置键值 | 墙面量化键 |
| `cabinetQuantKeys` | `string[]` | 内置键值 | 橱柜量化键 |
| `secondarySemanticNames` | `string[]` | `garageDoor, roof, eave` | 次要语义名称 |
| `secondaryMinPixelRatio` | `number` | `0.002` | 次要语义的最小像素比例 |
| `junctionHRadiusPx` | `number` | `24` | 踢脚线接缝水平半径 |
| `junctionVRadiusPx` | `number` | `2` | 踢脚线接缝垂直半径 |
| `kickBridgeHalfWPx` | `number` | `6` | 踢脚线水平间隙桥接半宽 |
| `baseboardJunctionRowMarginPx` | `number` | `1` | 踢脚线接缝行边距 |
| `baseboardJunctionVReachPx` | `number` | `2` | 踢脚线接缝垂直延伸 |
| `baseboardMinRunPx` | `number` | `2` | 遮罩条带最小运行长度 |
| `splitWalls` | `boolean` | `false` | 将墙面遮罩按纹理边界拆分为 `wall-1`、`wall-2`... |
| `splitWallsMaxCount` | `number` | `8` | 最大墙面子区域数 |
| `splitWallsMinAreaRatio` | `number` | `0.002` | 碎片最小面积比例(相对于总分割像素) |
| `splitWallsColorDistSq` | `number` | `1400` | 连通分量色度均值距离平方阈值 |
| `splitWallsChromaBlurRadius` | `number` | `5` | 保留:色度平滑半径 |
| `splitWallsNeutralChromaMax` | `number` | `14` | 白/灰墙面低色度半径;与彩色墙面的强制边界 |
启用 `splitWalls` 后,单个 `wall` 区域将被替换为多个 `wall-N` 子区域,每个子区域可独立上色和撤销。旧会话中 `regionName: 'wall'` 的记录无法映射到新的子区域名称,需重新上色。

View File

@ -0,0 +1,21 @@
---
id: paint-config
title: "PropspaintConfig"
---
# 🖌️ PropspaintConfig
| 字段 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `palette` | `BgrColor[]` | 6色内置调色板 | 底部画笔颜色条 |
| `colorBaseOpacity` | `number` | `0.88` | 基础颜色不透明度 |
| `lLightOpacity` | `number` | `0.50` | L 通道叠加强度 |
| `textureOpacity` | `number` | `0.85` | 高频纹理叠加强度(更强的纹理保留效果) |
| `lLowBlurKernel` | `number` | `7` | 低频高斯核(奇数) |
| `lLowContrast` | `number` | `1.15` | 低频对比度 |
| `lLowBrightness` | `number` | `0.9` | 低频亮度 |
| `lHighGain` | `number` | `1.22` | 高频增益 |
| `maskFeatherColor` | `number` | `1.6` | 上色边缘羽化(颜色)— 软边缘 alpha 半径,单位像素 |
| `maskFeatherTexture` | `number` | `0.9` | 上色边缘羽化(纹理)— 保留/辅助 |
| `regionOverlayFill` | `string` | `rgba(20,120,235,0.58)` | 虚线 / 高亮填充颜色 |
| `regionOutlineStrokeWidth` | `number` | `4` | 虚线轮廓描边宽度 |

View File

@ -0,0 +1,16 @@
---
id: pipeline-config
title: "PropspipelineConfig"
---
# 🔬 PropspipelineConfig
| 字段 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `maxImageLongSide` | `number` | `720` | 分割 / pickMap / 工作区域缩放的最大长边 |
| `paintFreqMaxLongSide` | `number` | `480` | OpenCV LAB 频域层的最大长边 |
| `originPreviewMaxLongSide` | `number` | `360` | 预览最大长边(主路径使用工作分辨率) |
| `maskPathMaxLongSide` | `number` | `480` | 轮廓路径下采样的最大长边 |
| `minContourArea` | `number` | `100` | 最小轮廓面积(按分辨率比例缩放) |
| `contourApproxEpsilon` | `number` | `0.003` | 轮廓多边形近似系数 |
| `maxRegions` | `number` | `500` | 分割期间的最大区域数 |

View File

@ -0,0 +1,18 @@
---
id: props-image
title: "Props图像与初始化"
---
# 🖼️ Props图像与初始化
| Prop | 类型 | 必填 | 默认值 | 描述 |
| --- | --- | --- | --- | --- |
| `originUrl` | `string` | 是* | — | 原始图像 URL`file://`、绝对路径或 `http(s)://` |
| `maskUrl` | `string` | 是* | — | 遮罩图像 URL语义色块图建议与原始图像尺寸相同 |
| `originImgPath` | `string` | — | — | **已弃用** — 请使用 `originUrl` |
| `maskImgPath` | `string` | — | — | **已弃用** — 请使用 `maskUrl` |
| `initialSession` | `MaskSegmentSession` | 否 | — | 从 MMKV 等恢复的草稿;区域就绪后自动调用 `loadSession` |
| `initialPaintColor` | `BgrColor` | 否 | — | **可选**。初始自定义画笔颜色 `{ b, g, r }`;省略时默认不选中画笔;用户需选择颜色或调用 `ref.setPaintColor` |
| `initialPaintConfigJson` | `Record<string, unknown>` | 否 | — | **可选**。`initialPaintColor` 的附带画笔配置;成功上色时通过 `onPaintCallback` 返回 |
\* 使用时至少需要 `originUrl` / `maskUrl` 之一。

View File

@ -0,0 +1,25 @@
---
id: props-semantic
title: "Props语义颜色与轮廓"
---
# 🎨 Props语义颜色与轮廓
| Prop | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `semanticColors` | `MaskSemanticColor[]` | `MASK_SEMANTIC_COLORS` | 遮罩语义识别颜色;等同于 `maskConfig.semanticColors` |
| `regionOutlineColor` | `string` | `rgba(20, 120, 235, 0.58)` | 区域虚线高亮颜色;等同于 `paintConfig.regionOverlayFill` |
顶层 props 优先于嵌套的 `maskConfig` / `paintConfig`
## MaskSemanticColor 结构
```ts
{
name: string; // 语义名称,如 wall / ceiling / baseboard
hex: string; // 显示用的十六进制颜色
bgr: { b: number; g: number; r: number }; // 必须与遮罩像素 BGR 通道匹配
}
```
内置调色板:`MASK_SEMANTIC_COLORS`(详见 `src/utils/maskSemanticPalette.ts`)。

View File

@ -0,0 +1,52 @@
---
id: ref-methods
title: "Ref 方法"
---
# 🔧 Ref 方法
通过 `ref` 访问(类型 `MaskSegmentCanvasRef`
| 方法 | 签名 | 描述 |
| --- | --- | --- |
| `reset` | `() => void` | 撤销上一步上色操作(按 `paintHistory` |
| `swap` | `(showOrigin?: boolean) => void` | 切换原始图像对比;省略参数则切换,`true`/`false` 强制设置 |
| `save` | `(options?) => Promise<SavePaintResult>` | 合成并保存 PNG`options.destDir` 可选输出目录 |
| `session` | `() => MaskSegmentSession` | 导出 JSON 可序列化会话(用于 MMKV 存储) |
| `loadSession` | `(session) => void` | 恢复上色状态(也可通过 `initialSession` 使用) |
| `setPaintColor` | `(color, configJson?) => void` | 设置当前画笔颜色;清除底部颜色条选中状态 |
| `setMaskConfig` | `(config) => void` | 运行时更新遮罩配置并 **重新分割** |
| `clearAllPaint` | `() => void` | 清除所有上色记录 |
| `resegment` | `() => Promise<void>` | 清除 PNG 缓存并重新分割 |
| `getRegions` | `() => SegmentRegion[]` | 当前区域列表快照 |
| `getPaintedRegions` | `() => PaintedRegionRecord[]` | 当前上色记录快照 |
## SavePaintResult
`{ filePath, width, height, paintedCount, previewPath? }`
## 代码示例
```tsx
const ref = useRef<MaskSegmentCanvasRef>(null);
ref.current?.reset();
ref.current?.swap(); // 切换
ref.current?.swap(true); // 强制显示原始图像
const result = await ref.current?.save({ destDir: '/path/to/dir' });
const session = ref.current?.session();
ref.current?.loadSession(session);
ref.current?.setPaintColor({ b: 100, g: 120, r: 140 }, { sku: 'paint-001' });
ref.current?.setMaskConfig({ semanticColors: customColors });
ref.current?.clearAllPaint();
await ref.current?.resegment();
const regions = ref.current?.getRegions();
const painted = ref.current?.getPaintedRegions();
```
> `save` 依赖于工作缓冲区和 pickMap 就绪(通常在 `interactive` 之后);如果未就绪则抛出 `'Image not ready, cannot save'`

View File

@ -0,0 +1,26 @@
---
id: storage
title: "存储约定"
---
# 💾 存储约定
| 能力 | 推荐存储 | 内容 |
| --- | --- | --- |
| `ref.save()` | 文件系统 | 全分辨率 PNG 路径 |
| `ref.session()` | MMKV / AsyncStorage | JSON 元数据URL、上色记录、画笔颜色等 |
## MaskSegmentSession 结构
```ts
{
version: 1;
originUrl: string;
maskUrl: string;
painted: PaintedRegionRecord[]; // { regionId, regionName, color, configJson? }
paintHistory: number[];
currentColor?: BgrColor;
currentColorConfigJson?: Record<string, unknown>;
savedAt: number;
}
```

View File

@ -0,0 +1,24 @@
---
id: ui-controls
title: "PropsUI 控件与样式"
---
# 🎛️ PropsUI 控件与样式
| Prop | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `showToolbar` | `boolean` | `true` | 顶部工具栏("清除缓存并重新分割" |
| `showColorBar` | `boolean` | `true` | 底部画笔颜色条 |
| `showStatusRow` | `boolean` | `true` | 分割/加载状态文字 |
| `showOverlayButtons` | `boolean` | `true` | 左下撤销、右下对比按钮 |
| `showDebugPickers` | `boolean` | `true` | 相册调试选择器(生产环境设为 `false` |
| `disabled` | `boolean` | `false` | 禁用上色交互 |
| `style` | `ViewStyle` | — | 外层容器样式 |
| `canvasStyle` | `ViewStyle` | — | 画布区域样式 |
| `undoButtonStyle` / `compareButtonStyle` | `ViewStyle` | — | 覆盖按钮样式 |
| `undoButtonTextStyle` / `compareButtonTextStyle` | `TextStyle` | — | 覆盖按钮文字样式 |
| `undoButtonText` | `string` | `Undo` | 撤销按钮标签 |
| `compareButtonText` | `string` | `Compare` | 进入对比模式标签 |
| `compareExitButtonText` | `string` | `Exit Compare` | 退出对比模式标签 |
| `renderUndoButton` | `(props) => ReactNode` | — | 自定义撤销按钮渲染器 |
| `renderCompareButton` | `(props) => ReactNode` | — | 自定义对比按钮渲染器 |

View File

@ -0,0 +1,203 @@
---
id: basic-usage
title: 基本用法
---
# 💡 基本用法
## 🧑‍💻 最小示例
一个可直接复制粘贴的完整示例,涵盖 **PNG 预热**、**状态管理**、**配置**、**加载状态**、**onWatch** 和 **ref 操作**
```tsx
import React, { useEffect, useRef, useState } from 'react';
import { ActivityIndicator, Text, View } from 'react-native';
import MaskSegmentCanvas, {
type MaskSegmentCanvasRef,
type MaskSegmentSession,
type MaskSegmentWatchState,
type BgrColor,
MASK_SEMANTIC_COLORS,
prewarmPngBgrCacheAsync,
} from 'react-native-mask-segment-canvas';
/** 由宿主应用准备的图像路径(本地 file:// 或 http(s):// */
type ImagePaths = {
origin: string;
mask: string;
};
const INTERACTIVE_STATES: MaskSegmentWatchState[] = [
'interactive',
'mask_paths_ready',
];
export function PaintScreen() {
const canvasRef = useRef<MaskSegmentCanvasRef>(null);
const [imagePaths, setImagePaths] = useState<ImagePaths | null>(null);
const [pathsError, setPathsError] = useState('');
const [watchState, setWatchState] = useState<MaskSegmentWatchState | ''>('');
const [errorMessage, setErrorMessage] = useState('');
const [sessionDraft] = useState<MaskSegmentSession | null>(null);
const isInteractive = INTERACTIVE_STATES.includes(
watchState as MaskSegmentWatchState,
);
const isOutlineReady = watchState === 'mask_paths_ready';
const isCanvasLoading =
imagePaths != null &&
watchState !== '' &&
!INTERACTIVE_STATES.includes(watchState as MaskSegmentWatchState) &&
watchState !== 'error';
useEffect(() => {
let cancelled = false;
(async () => {
try {
const origin = 'file:///path/to/origin.png';
const mask = 'file:///path/to/mask.png';
await prewarmPngBgrCacheAsync([origin, mask]);
if (!cancelled) {
setImagePaths({ origin, mask });
}
} catch (e) {
if (!cancelled) {
setPathsError(e instanceof Error ? e.message : String(e));
}
}
})();
return () => { cancelled = true; };
}, []);
const handleSave = async () => {
if (!isInteractive) return;
const result = await canvasRef.current?.save({ destDir: undefined });
console.log('saved', result?.filePath, result?.paintedCount);
};
if (pathsError) {
return <Text>{pathsError}</Text>;
}
if (!imagePaths) {
return (
<View>
<ActivityIndicator />
<Text>Waiting for origin and mask images...</Text>
</View>
);
}
return (
<View style={{ flex: 1 }}>
{isCanvasLoading ? <Text>Loading: {watchState}</Text> : null}
{watchState === 'interactive' ? (
<Text>Paintable (outlines loading...)</Text>
) : null}
{isOutlineReady ? <Text>Ready</Text> : null}
{errorMessage ? <Text>{errorMessage}</Text> : null}
<MaskSegmentCanvas
ref={canvasRef}
style={{ flex: 1 }}
originUrl={imagePaths.origin}
maskUrl={imagePaths.mask}
semanticColors={MASK_SEMANTIC_COLORS}
regionOutlineColor="rgba(20, 120, 235, 0.58)"
maskConfig={{ blackThreshold: 30, maxRegionColors: 6 }}
pipelineConfig={{ maxImageLongSide: 720 }}
paintConfig={{ colorBaseOpacity: 0.88 }}
interactionConfig={{
initRegionFlashMs: 1000,
enableInitRegionFlash: true,
}}
initialSession={sessionDraft ?? undefined}
showDebugPickers={false}
showToolbar={false}
showColorBar
showStatusRow={false}
showOverlayButtons
disabled={!isInteractive}
onWatch={(state, durationMs, detail) => {
setWatchState(state);
console.log('[onWatch]', state, durationMs, detail);
}}
onPaintCallback={payload => {
if (payload.kind === 'brush_required') {
toast(payload.hint);
return;
}
console.log('painted', payload.regionId, payload.regionName, payload.color, payload.configJson);
}}
onError={message => {
setErrorMessage(message);
setWatchState('error');
}}
/>
</View>
);
}
```
---
## 📊 状态变量
| 状态 | 类型 | 用途 |
| --- | --- | --- |
| `imagePaths` | `{ origin, mask } \| null` | 宿主解析后的本地/远程图片路径 |
| `pathsError` | `string` | 路径解析或 PNG 预热失败时的错误信息 |
| `watchState` | `MaskSegmentWatchState \| ''` | `onWatch` 上报的初始化阶段 |
| `isInteractive` | 派生值 | `interactive``mask_paths_ready` 时为 `true` — 允许操作 |
| `isOutlineReady` | 派生值 | `mask_paths_ready` 时为 `true` — 轮播虚线轮廓已就绪 |
| `isCanvasLoading` | 派生值 | Canvas 初始化阻塞中(不包括等待 PNG 路径) |
| `errorMessage` | `string` | `onError` 写入的分割/加载失败信息 |
| `sessionDraft` | `MaskSegmentSession \| null` | 从 MMKV 或类似存储恢复的草稿 |
---
## ⚙️ 选择配置值
| 配置 | 使用顶层 prop 的场景 | 使用嵌套 Config 的场景 |
| --- | --- | --- |
| 语义颜色 | `semanticColors={...}` 多数情况使用 | `maskConfig.semanticColors` 与其他遮罩参数配合使用时 |
| 轮廓颜色 | `regionOutlineColor="..."` 多数情况使用 | `paintConfig.regionOverlayFill` 同时自定义画笔调色板时 |
| 黑色阈值、最大区域数 | — | `maskConfig` |
| 图像处理尺寸 | — | `pipelineConfig` |
| 闪烁间隔、点击容差 | — | `interactionConfig` |
顶层 props 和嵌套 Configs **可以共存**;顶层 `semanticColors` / `regionOutlineColor` 优先级更高。
---
## 🔄 watchState 与 UI 引导
```ts
// 阻塞加载(区域和上色层就绪之前)
const isLoading = ![
'interactive',
'mask_paths_ready',
'error',
'',
].includes(watchState);
// 允许点击区域、选择颜色、上色(无需等待轮廓路径)
const canOperate =
watchState === 'interactive' || watchState === 'mask_paths_ready';
// 轮播虚线轮廓已完全就绪(可选 — 可关闭"轮廓准备中"提示)
const isOutlineReady = watchState === 'mask_paths_ready';
// 显示错误界面
const hasError = watchState === 'error';
```
`interactive` 状态下,`detail.maskPathsReady` 通常为 `false``mask_paths_ready` 状态下为 `true`。间隔约 100ms异步 Skia 路径构建),不阻塞点击上色。
`originUrl` / `maskUrl` 支持:
- 本地路径:`file:///...` 或绝对路径
- 远程 URL`http(s)://...`(组件内部处理下载和解码)
> 旧版 props `originImgPath` / `maskImgPath` 已弃用;请使用 `originUrl` / `maskUrl`

View File

@ -0,0 +1,17 @@
---
id: dependencies
title: 依赖项
---
# 📚 依赖项
| 包 | 用途 |
| --- | --- |
| `@shopify/react-native-skia` | Canvas 渲染、Path、虚线描边、Blend 合成 |
| `react-native-fast-opencv` | 遮罩形态学、轮廓处理 |
| `react-native-fs` | 图层缓存、PNG 保存 |
| `react-native-image-picker` | Demo 相册选择器(可选) |
| `react-native-reanimated` | Skia 动画依赖 |
| `react-native-safe-area-context` | 安全区域边距(可选) |
| `buffer` | 二进制 buffer polyfill |
| `upng-js` | 纯 JS PNG 编解码 |

View File

@ -0,0 +1,25 @@
---
id: custom-colors
title: 自定义语义颜色表
---
# 🎨 自定义语义颜色表
当遮罩使用不同的颜色值时,可以定义自定义语义颜色:
```tsx
const gymColors: MaskSemanticColor[] = [
{ name: 'wall', hex: '#4363D8', bgr: { b: 216, g: 99, r: 67 } },
{ name: 'ceiling', hex: '#3CB44B', bgr: { b: 75, g: 180, r: 60 } },
// ...
];
<MaskSegmentCanvas
semanticColors={gymColors}
maskConfig={{ blackThreshold: 30, maxRegionColors: 6 }}
/>
```
:::caution
`semanticColors` 必须与后端/标注遮罩中使用的语义颜色匹配;不匹配会导致识别偏差。
:::

View File

@ -0,0 +1,18 @@
---
id: draft-recovery
title: 草稿恢复
---
# 💾 草稿恢复
```tsx
const draft = JSON.parse(mmkv.getString('paint_draft'));
<MaskSegmentCanvas
originUrl={draft.originUrl}
maskUrl={draft.maskUrl}
initialSession={draft}
/>
```
使用 `ref.session()` 导出当前会话并存储在 MMKV 或 AsyncStorage 中,以便后续恢复。

View File

@ -0,0 +1,21 @@
---
id: local-paths
title: 从 API 传入本地路径
---
# 🌐 从 API 传入本地路径
```tsx
<MaskSegmentCanvas
originUrl={localOriginPath}
maskUrl={localMaskPath}
showDebugPickers={false}
showToolbar={false}
semanticColors={MASK_SEMANTIC_COLORS}
regionOutlineColor="#1e96ff"
onWatch={(state, ms, detail) => {
if (state === 'interactive') hideBlockingLoader();
if (state === 'mask_paths_ready') hideOutlineHint();
}}
/>
```

View File

@ -0,0 +1,19 @@
---
id: png-pre-warm
title: PNG 缓存预热(推荐)
---
# 🔥 PNG 缓存预热(推荐)
在挂载 Canvas 前预热 PNG 解码缓存,可节省 **100250ms** 的初始化时间。
```tsx
import { prewarmPngBgrCacheAsync } from 'react-native-mask-segment-canvas';
async function openPaintScreen(originUrl: string, maskUrl: string) {
await prewarmPngBgrCacheAsync([originUrl, maskUrl]);
navigation.navigate('Paint', { originUrl, maskUrl });
}
```
在下载/提取图片后、导航到上色界面前调用 `prewarmPngBgrCacheAsync` 可获得最佳性能。

View File

@ -0,0 +1,107 @@
---
id: installation
title: 安装
---
# 📦 安装
## Peer 依赖
在宿主项目中安装以下依赖(版本应与宿主 RN 版本匹配):
```bash
npm install @shopify/react-native-skia react-native-reanimated react-native-fast-opencv react-native-fs buffer upng-js react-native-gesture-handler
# 如果使用 showDebugPickers相册选择器
npm install react-native-image-picker
# 安全区域边距
npm install react-native-safe-area-context
```
## 安装后设置
本库依赖 `patch-package` 来修补 `react-native-fast-opencv`。宿主项目的 `package.json` 必须包含:
```json
{
"scripts": {
"postinstall": "patch-package"
},
"devDependencies": {
"patch-package": "^8.0.1"
}
}
```
安装本库后,`node_modules/react-native-mask-segment-canvas/patches/` 中的补丁将在宿主 `postinstall` 期间自动应用。
## iOS / Android 原生依赖
```bash
cd ios && pod install && cd ..
```
确保宿主项目已按照各库文档完成 Skia、Reanimated 和 OpenCV 的原生设置。
## Metro 配置
使用 `npm link`、monorepo 或 `file:` 依赖时,请将本库添加到 `watchFolders`,并使用 `extraNodeModules` + `blockList` 防止重复模块解析:
```js
const path = require('path');
module.exports = {
watchFolders: [path.resolve(__dirname, '../MaskSegmentApp')],
resolver: {
nodeModulesPaths: [path.resolve(__dirname, 'node_modules')],
extraNodeModules: {
'react-native-reanimated': path.resolve(__dirname, 'node_modules/react-native-reanimated'),
'@shopify/react-native-skia': path.resolve(__dirname, 'node_modules/@shopify/react-native-skia'),
'react-native-gesture-handler': path.resolve(__dirname, 'node_modules/react-native-gesture-handler'),
'react-native-fast-opencv': path.resolve(__dirname, 'node_modules/react-native-fast-opencv'),
'react-native-safe-area-context': path.resolve(__dirname, 'node_modules/react-native-safe-area-context'),
'react-native-fs': path.resolve(__dirname, 'node_modules/react-native-fs'),
},
blockList: [
/\/MaskSegmentApp\/node_modules\/@shopify\/react-native-skia\//,
/\/MaskSegmentApp\/node_modules\/react-native-reanimated\//,
/\/MaskSegmentApp\/node_modules\/react-native-fast-opencv\//,
/\/MaskSegmentApp\/node_modules\/react-native-gesture-handler\//,
/\/MaskSegmentApp\/node_modules\/react-native-safe-area-context\//,
/\/MaskSegmentApp\/node_modules\/react-native-fs\//,
],
},
};
```
**强烈推荐** — 在宿主 `index.js` 最顶部(任何业务代码之前)添加:
```js
import '@shopify/react-native-skia';
```
完整的配置(含所有 peer singleton 包)请参考 `example/metro.config.js``example/index.js`
## 故障排除:重复模块错误
常见症状:
- `SkiaPictureView must be a function (received 'undefined')`
- `createAnimatedNode: Animated node[...] already exists`
这些问题几乎都是由于 Metro 解析了多份 reanimated / skia / gesture-handler / fast-opencv / safe-area 包副本导致的。
**最佳实践:**
1. 从 `example/metro.config.js` 复制 `singletonPackages` + `extraNodeModules` + `blockList` 模式
2. 在 `index.js` 顶部按顺序导入 gesture-handler → reanimated → skia
3. 使用 `--reset-cache` 重启 Metro 并重新安装应用
详细清单和模板请参阅示例项目。
### 集成方式
| 方式 | 说明 |
| --- | --- |
| `npm install` | 生产环境推荐 |
| `npm link` | 本地开发 |
| `file:..` | 相对路径依赖 |

View File

@ -0,0 +1,13 @@
---
id: interaction-guide
title: 交互指南
---
# 🎮 交互指南
1. 🔁 **初始轮播**:区域就绪后,每个区域的虚线轮廓按 `initRegionFlashMs`(默认 1s依次闪烁首次用户触摸时停止。
2. 🔍 **预览(未选择画笔)**:长按区域可显示触摸点下连通分量的虚线轮廓;点击黑色区域不显示轮廓。
3. 🎨 **上色(已选择画笔)**:点击底部颜色条中的颜色或调用 `ref.setPaintColor`(或通过 `initialPaintColor` 预设),然后点击区域上色;再次点击同一区域会覆盖颜色。
4. 💬 **无画笔点击**:不执行上色;`onPaintCallback` 触发 `kind: 'brush_required'`,携带提示信息和目标区域信息,供宿主显示 Toast/弹窗提示选择颜色。
5. ↩️ **撤销**:左下按钮或 `ref.reset()`;按上色历史逐步后退。
6. 👁️ **与原图对比**:右下按钮或 `ref.swap()`;隐藏上色层以显示原图。

View File

@ -0,0 +1,50 @@
---
id: intro
title: 概述
---
# 🎨 react-native-mask-segment-canvas
一个 React Native **0.79** 交互式遮罩分割库。核心导出为 `MaskSegmentCanvas` 组件,可通过 **npm 包****npm link** 在任何 React Native 项目中使用。
- 🧠 **OpenCV** (`react-native-fast-opencv`):遮罩语义布局、踢脚线修补、区域提取
- 🖌️ **Skia RuntimeEffect (SkSL)**:单 Pass 全屏着色器,混合原图 + LAB 低频/高频纹理颜色叠加
- ✂️ **Skia Path**:区域虚线轮廓高亮
- 👆 **交互**:底部颜色条选择画笔(可选初始化)→ 点击区域上色;未选择画笔时点击会触发 `onPaintCallback` 并附带提示;未选画笔时长按可预览区域的虚线轮廓
本仓库同时作为 **库源码**`src/index.ts`)和 **自测 Demo**(根目录 `App.tsx`)。
📌 **推荐的集成 Demo 请查看 `example/` 目录** — 该目录仅使用公开 API完全模拟消费方项目的集成方式包括 `package.json`、Metro 配置和完整的参考 `App.tsx`)。
---
## 🔭 概述
`MaskSegmentCanvas` 渲染原始图像并叠加语义遮罩,允许用户点击区域并应用颜色。处理流程:
1. 📥 **加载**原始图像和遮罩图像(本地 `file://` 或远程 `http(s)://`
2. 🧩 **分割**通过 OpenCV 将遮罩分割为语义区域(墙面、天花板、踢脚线等)
3. 🎨 **准备**通过 SkSL 生成 LAB 频域层纹理,实现逼真的颜色混合
4. 📐 **构建**每个区域的 Skia 虚线轮廓路径
5. 👆 **交互** — 用户选择画笔颜色并点击区域上色;上色层保留底层纹理
6. 💾 **保存**合成结果为 PNG导出 JSON 会话用于草稿恢复
组件通过 `onWatch` 发出 Pipeline 状态转换,宿主应用可据此显示相应的加载状态。
---
## 📋 环境要求
- 🟢 Node.js >= 18推荐 20+
- 🍎 Xcode 15+iOS
- 🤖 Android Studio + JDK 17Android
- 📦 CocoaPodsiOS
---
## 🚀 下一步
- **[安装](/docs/installation)** — 在项目中配置库
- **[快速开始](/docs/quick-start)** — 运行开发 Demo
- **[基本用法](/docs/basic-usage)** — 入门最小示例
- **[API 参考](/docs/api)** — 完整的 Props 和方法文档

View File

@ -0,0 +1,11 @@
---
id: notes
title: 注意事项
---
# 📝 注意事项
- 遮罩图像应为与原始图像同尺寸的语义色块图(黑色背景 + 纯色区域)。`max(B,G,R) < blackThreshold`默认 30的像素将被排除在分割之外
- OpenCV 分割在 JS 线程上运行;非常大的图像可能导致掉帧。使用 `pipelineConfig.maxImageLongSide` 限制处理分辨率。
- iOS 相册访问需要照片权限(仅在启用 `showDebugPickers` 时需要)。
- `semanticColors` 必须与后端/标注遮罩中使用的语义颜色匹配;不匹配会导致识别偏差。

View File

@ -0,0 +1,92 @@
---
id: performance
title: 性能
---
# ⚡ 性能
以下数据基于 Demo 测试图片(`assets/test/origin.png` **1080×1920**6 个语义区域)、**默认 `pipelineConfig`** 和 `onWatch` `durationMs`(从 `init` 开始测量)。这些是 **经验范围数据**,非严格基准测试;实际设备结果因 CPU、存储和 RN 版本而异。
## 实测参考(开发环境 + PNG 预热)
Demo 在挂载 Canvas 前调用 `prewarmPngBgrCacheAsync([origin, mask])`,因此 PNG 解码命中内存缓存。典型日志:
| 阶段 | watchState | 大约耗时 | 备注 |
| --- | --- | --- | --- |
| 遮罩对齐 | `mask_aligned` | ~160ms | 遮罩缩放至分割工作分辨率 |
| 区域就绪 | `regions_ready` / `mask_sampled` | ~320ms | 布局扫描 + 踢脚线 + pickMap |
| **可交互** | `**interactive`** | **~320450ms** | 可点击区域、选择颜色、Shader 上色 |
| 轮廓就绪 | `mask_paths_ready` | ~430550ms | `interactive` 后约 100ms轮播轮廓可显示 |
`interactive` **不等待**轮廓路径;`mask_paths_ready` 仅影响初始轮播和可选的 UI 提示。
同图子步骤耗时大小(`__DEV__` 日志,默认 pipeline
| 子步骤 | 大约耗时 | 工作分辨率 |
| --- | --- | --- |
| OpenCV LAB 高/低频 | ~1040ms | 270×480 |
| 高/低频 Skia 纹理 | ~2030ms | 同上 |
| 布局扫描 + 踢脚线 + pick 表 | ~90120ms | 405×7201080p → longSide 720 |
| 全轮廓路径(异步,非阻塞) | ~80150ms | 270×480 |
## 分辨率与 pipelineConfig
计算密集型步骤受 **最大长边限制** 约束,**不随 4K/8K 原图线性增长**。**完整 PNG 解码**仍随像素数线性增长。
| 步骤 | 配置键 | 1080×1920 实际尺寸 | 随原图像素数增长 |
| --- | --- | --- | --- |
| PNG 解码 | — | 1080×1920 × 2 张图片 | **是** |
| 遮罩分割 / pickMap | `maxImageLongSide: 720` | ~405×720 | **否**(长边 >720 时固定) |
| Shader 高/低频 | `paintFreqMaxLongSide: 480` | ~270×480 | **否** |
| 工作区 Skia 原图 | 同 `maxImageLongSide` | ~405×720 | **否** |
| 虚线轮廓 | `maskPathMaxLongSide: 480` | ~270×480 | **否**(不阻塞 `interactive` |
## interactive 预估(默认 Pipeline
| 原始图像规格 | 相对于 1080p 像素 | PNG 预热后 | 冷启动(无预热) |
| --- | --- | --- | --- |
| 1080×1920 | 1× | **320450ms** | **450700ms** |
| 1440×25602K | ~1.8× | **400550ms** | **600900ms** |
| 3840×21604K | ~4× | **500750ms** | **8001200ms** |
| 7680×43208K | ~16× | **0.81.5s** | **1.53s+** |
> **`<300ms` interactive**1080p + 预热 + 默认 pipeline + 高端设备上可达,但属 **乐观估计** — 不应视为全设备 SLA。
## 设备等级1080p默认 Pipeline
相对于约 320ms 的开发环境基线:
| 等级 | 相对倍数 | 预热后 `interactive` | 冷启动 |
| --- | --- | --- | --- |
| 旗舰 iOS / 新款旗舰 Android | 0.81.2× | 300450ms | 500800ms |
| 中端 Android | 1.52.5× | 500800ms | 700ms1.2s |
| 低端 Android4GB旧 SoC | 2.54× | 800ms1.3s | 12s+ |
Android 额外开销主要来自JS ↔ OpenCV 桥接、内存带宽/GC、Skia 纹理上传。
## 提高 maxImageLongSide 的影响
`pipelineConfig.maxImageLongSide` 设为 **1280**(高于默认 720会使分割工作区变为约 720×1280像素数约为 720 档的 **3 倍**
| 场景 | 默认 720 | 提高到 1280 |
| --- | --- | --- |
| 1080p `interactive`(中端设备) | ~320800ms | **500ms1s+** |
| 分割 / pickMap 耗时 | ~90120ms | ~250350ms |
更高精度带来更长的初始化时间。要保持在 **`<500ms` interactive**,保留默认 **720**;必要时可降至 **640**
## 优化建议
1. 🚀 **PNG 预热(推荐)**:在下载/提取图片后、导航到上色界面前调用 `prewarmPngBgrCacheAsync`。通常可节省 **100250ms**(低端设备收益最大)。
```tsx
import { prewarmPngBgrCacheAsync } from 'react-native-mask-segment-canvas';
await prewarmPngBgrCacheAsync([originPath, maskPath]);
// 然后挂载 MaskSegmentCanvas
```
2. ⏱️ **加载时机**:在 `interactive` 时关闭阻塞加载器;可选监听 `mask_paths_ready` 以显示"轮廓准备中"提示。
3. 🖼️ **大图 / 低端设备**:保持默认 `maxImageLongSide: 720`;可选将 `paintFreqMaxLongSide` 降至 **360**
4. 📷 **4K 素材**:在宿主侧先降采样再传入,或接受约 0.81.5s 的 `interactive`(含预热)。
5. 🔍 **可观测性**:观察 Metro 日志中的 `[MaskSegment]`、`[⏱ ...]` 前缀和 `onWatch` `durationMs`

View File

@ -0,0 +1,30 @@
---
id: project-structure
title: 项目结构
---
# 📁 项目结构
```
MaskSegmentApp/ # 仓库根目录npm 包 react-native-mask-segment-canvas
├── App.tsx # 开发自测 Demo直接从 ./src 导入)
├── src/
│ ├── index.ts # 包入口消费方import 'react-native-mask-segment-canvas'
│ ├── components/
│ │ ├── MaskSegmentCanvas.tsx
│ │ └── MaskSegmentCanvas.types.ts
│ └── utils/
│ ├── maskSegmentation.ts
│ ├── maskSegmentRuntime.ts
│ ├── maskSemanticPalette.ts
│ └── ...
├── example/ # ★ 推荐:消费方集成 Demo
│ ├── App.tsx # 仅使用公开 API 的完整示例
│ ├── index.js / app.json
│ ├── package.json # 所需依赖 + "react-native-mask-segment-canvas": "file:.."
│ ├── metro.config.js / babel.config.js / tsconfig.json
│ └── README.md # 如何在真实项目中集成
├── patches/ # 随包发布;由宿主 postinstall 应用
├── ios/ # 根 Demo 原生项目(不发布到 npm
└── android/
```

View File

@ -0,0 +1,25 @@
---
id: quick-start
title: 快速开始(开发 Demo
---
# 🚀 快速开始(开发 Demo
根目录 `App.tsx` 是一个完整的自测 Demo直接从 `./src` 导入。
```bash
cd MaskSegmentApp
npm install
cd ios && bundle exec pod install && cd ..
npm start
# 在另一个终端中
npm run ios
# 或
npm run android
```
**查看消费方项目如何集成:** 进入 `example/` 目录并按照其中的 `README.md` 操作。它使用 `import from 'react-native-mask-segment-canvas'` 配合标准 `package.json` 和 Metro 配置,完全模拟消费方环境。

View File

@ -0,0 +1,42 @@
---
id: troubleshooting
title: 故障排除
---
# 🔧 故障排除
## iOS pod install 失败
```bash
cd ios
bundle install
bundle exec pod install --repo-update
```
## Android 构建错误
```bash
cd android && ./gradlew clean && cd ..
```
## 分割失败 / 零区域
- 确认 `originUrl` / `maskUrl` 可访问
- 确认遮罩语义颜色与 `semanticColors` 配置匹配
- 检查 Metro 日志中的 `[MaskSegment]` / `[⏱ ...]` 输出
## 虚线轮廓错位 / 多余轮廓
- 轮廓从遮罩像素外部轮廓生成;长按仅显示触摸点下的连通分量
- 初始轮播仅显示每个语义区域的最大连通分量
## 常见重复模块错误
**症状:**
- `SkiaPictureView must be a function (received 'undefined')`
- `createAnimatedNode: Animated node[...] already exists`
**解决方案:**
1. 从 `example/metro.config.js` 复制 `singletonPackages` + `extraNodeModules` + `blockList` 模式
2. 在 `index.js` 顶部按顺序导入 gesture-handler → reanimated → skia
3. 使用 `--reset-cache` 重启 Metro 并重新安装应用

18283
docs/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

48
docs/package.json Normal file
View File

@ -0,0 +1,48 @@
{
"name": "mask-segment-canvas-docs",
"version": "0.3.0",
"private": true,
"scripts": {
"docusaurus": "docusaurus",
"start": "docusaurus start",
"build": "docusaurus build",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"clear": "docusaurus clear",
"serve": "docusaurus serve",
"write-translations": "docusaurus write-translations",
"write-heading-ids": "docusaurus write-heading-ids",
"typecheck": "tsc"
},
"dependencies": {
"@docusaurus/core": "3.7.0",
"@docusaurus/preset-classic": "3.7.0",
"@mdx-js/react": "^3.0.0",
"clsx": "^2.0.0",
"prism-react-renderer": "^2.3.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"webpack": "^5.95.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "3.7.0",
"@docusaurus/tsconfig": "3.7.0",
"@docusaurus/types": "3.7.0",
"typescript": "~5.6.0"
},
"browserslist": {
"production": [
">0.5%",
"not dead",
"not op_mini all"
],
"development": [
"last 3 chrome version",
"last 3 firefox version",
"last 5 safari version"
]
},
"engines": {
"node": ">=18.0"
}
}

85
docs/sidebars.ts Normal file
View File

@ -0,0 +1,85 @@
import type { SidebarsConfig } from '@docusaurus/plugin-content-docs';
const sidebars: SidebarsConfig = {
docsSidebar: [
{
type: 'doc',
id: 'intro',
label: 'Overview',
},
{
type: 'doc',
id: 'installation',
label: 'Installation',
},
{
type: 'doc',
id: 'quick-start',
label: 'Quick Start',
},
{
type: 'doc',
id: 'basic-usage',
label: 'Basic Usage',
},
{
type: 'category',
label: 'API Reference',
link: { type: 'doc', id: 'api/index' },
items: [
'api/props-image',
'api/props-semantic',
'api/mask-config',
'api/pipeline-config',
'api/paint-config',
'api/interaction-config',
'api/ui-controls',
'api/callbacks',
'api/ref-methods',
'api/storage',
],
},
{
type: 'doc',
id: 'interaction-guide',
label: 'Interaction Guide',
},
{
type: 'category',
label: 'Integration Examples',
items: [
'examples/png-pre-warm',
'examples/local-paths',
'examples/draft-recovery',
'examples/custom-colors',
],
},
{
type: 'doc',
id: 'project-structure',
label: 'Project Structure',
},
{
type: 'doc',
id: 'dependencies',
label: 'Dependencies',
},
{
type: 'doc',
id: 'performance',
label: 'Performance',
},
{
type: 'doc',
id: 'notes',
label: 'Notes',
},
{
type: 'doc',
id: 'troubleshooting',
label: 'Troubleshooting',
},
],
};
export default sidebars;

30
docs/src/css/custom.css Normal file
View File

@ -0,0 +1,30 @@
:root {
--ifm-color-primary: #1e78ff;
--ifm-color-primary-dark: #0167f4;
--ifm-color-primary-darker: #0161e6;
--ifm-color-primary-darkest: #004fbe;
--ifm-color-primary-light: #3a8aff;
--ifm-color-primary-lighter: #4893ff;
--ifm-color-primary-lightest: #72adff;
--ifm-code-font-size: 95%;
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
}
[data-theme='dark'] {
--ifm-color-primary: #4d95ff;
--ifm-color-primary-dark: #2b80ff;
--ifm-color-primary-darker: #1a76ff;
--ifm-color-primary-darkest: #005de7;
--ifm-color-primary-light: #6faaff;
--ifm-color-primary-lighter: #80b4ff;
--ifm-color-primary-lightest: #b3d2ff;
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
}
.hero--primary {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
}
[data-theme='dark'] .hero--primary {
background: linear-gradient(135deg, #0a0a1a 0%, #0d1528 50%, #081d3a 100%);
}

View File

@ -0,0 +1,99 @@
.heroBanner {
padding: 4rem 0;
text-align: center;
position: relative;
overflow: hidden;
}
.heroTitle {
font-size: 3rem;
font-weight: 800;
letter-spacing: -0.02em;
}
.heroSubtitle {
font-size: 1.5rem;
font-weight: 600;
margin-top: 0.5rem;
opacity: 0.9;
}
.heroDescription {
font-size: 1.1rem;
max-width: 640px;
margin: 1rem auto 0;
opacity: 0.75;
line-height: 1.7;
}
.buttons {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
margin-top: 2rem;
flex-wrap: wrap;
}
.features {
padding: 4rem 0;
background: var(--ifm-background-color);
}
.featureCard {
padding: 1.5rem;
text-align: center;
}
.featureEmoji {
font-size: 2.5rem;
margin-bottom: 0.75rem;
}
.featureTitle {
font-size: 1.2rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.featureDesc {
font-size: 0.95rem;
color: var(--ifm-color-emphasis-700);
line-height: 1.6;
}
.quickStart {
padding: 3rem 0 4rem;
background: var(--ifm-color-emphasis-100);
text-align: center;
}
[data-theme='dark'] .quickStart {
background: var(--ifm-color-emphasis-0);
}
.sectionTitle {
font-size: 2rem;
font-weight: 700;
margin-bottom: 1.5rem;
}
.codeBlock {
max-width: 640px;
margin: 0 auto;
text-align: left;
background: #1e1e2e;
border-radius: 8px;
padding: 1.25rem 1.5rem;
overflow-x: auto;
}
.codeBlock pre {
margin: 0;
}
.codeBlock code {
color: #cdd6f4;
font-size: 0.92rem;
font-family: 'Fira Code', 'JetBrains Mono', 'Cascadia Code', monospace;
}

140
docs/src/pages/index.tsx Normal file
View File

@ -0,0 +1,140 @@
import React from 'react';
import clsx from 'clsx';
import Link from '@docusaurus/Link';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import Layout from '@theme/Layout';
import Heading from '@theme/Heading';
import styles from './index.module.css';
function HeroSection() {
return (
<header className={clsx('hero hero--primary', styles.heroBanner)}>
<div className="container">
<Heading as="h1" className={styles.heroTitle}>
🎨 MaskSegmentCanvas
</Heading>
<p className={styles.heroSubtitle}>
React Native Interactive Mask Segmentation
</p>
<p className={styles.heroDescription}>
OpenCV semantic segmentation + SkSL Shader coloring a powerful
interactive mask painting library for React Native 0.79+
</p>
<div className={styles.buttons}>
<Link className="button button--secondary button--lg" to="/docs/intro">
Get Started
</Link>
<Link className="button button--outline button--lg" to="/docs/api">
API Reference
</Link>
</div>
</div>
</header>
);
}
function FeatureCard({ emoji, title, description }: { emoji: string; title: string; description: string }) {
return (
<div className={clsx('col col--4', styles.featureCard)}>
<div className={styles.featureEmoji}>{emoji}</div>
<Heading as="h3" className={styles.featureTitle}>{title}</Heading>
<p className={styles.featureDesc}>{description}</p>
</div>
);
}
function FeaturesSection() {
const features = [
{
emoji: '🧠',
title: 'OpenCV Segmentation',
description:
'Semantic mask layout, baseboard patching, and region extraction powered by react-native-fast-opencv.',
},
{
emoji: '🖌️',
title: 'Skia SkSL Shader',
description:
'Single-pass full-screen shader blending original image + LAB low/high frequency texture color overlays.',
},
{
emoji: '👆',
title: 'Rich Interaction',
description:
'Bottom color bar, tap-to-paint, long-press preview, undo, compare with original, and draft recovery.',
},
{
emoji: '📐',
title: 'Skia Dash Outlines',
description:
'Dashed outline highlights for each semantic region with automatic carousel animation.',
},
{
emoji: '⚡',
title: 'High Performance',
description:
'~320450ms to interactive on 1080p images. Independent of origin resolution with pipeline capping.',
},
{
emoji: '🌐',
title: 'Remote & Local Images',
description:
'Supports file:// absolute paths and http(s):// remote URLs with built-in download and decode.',
},
];
return (
<section className={styles.features}>
<div className="container">
<div className="row">
{features.map((feature, idx) => (
<FeatureCard key={idx} {...feature} />
))}
</div>
</div>
</section>
);
}
function QuickStartSection() {
return (
<section className={styles.quickStart}>
<div className="container">
<Heading as="h2" className={styles.sectionTitle}>Quick Start</Heading>
<div className={styles.codeBlock}>
<pre>
<code>{`npm install react-native-mask-segment-canvas
# Install peer dependencies
npm install @shopify/react-native-skia \\
react-native-reanimated \\
react-native-fast-opencv \\
react-native-fs buffer upng-js
# iOS
cd ios && pod install && cd ..`}</code>
</pre>
</div>
<div className={styles.buttons} style={{ marginTop: '1.5rem' }}>
<Link className="button button--primary button--lg" to="/docs/installation">
Full Installation Guide
</Link>
</div>
</div>
</section>
);
}
export default function Home(): JSX.Element {
const { siteConfig } = useDocusaurusContext();
return (
<Layout title={`${siteConfig.title} - React Native Mask Segmentation`} description="React Native interactive mask segmentation library with OpenCV semantic segmentation and Skia SkSL Shader coloring.">
<HeroSection />
<main>
<FeaturesSection />
<QuickStartSection />
</main>
</Layout>
);
}

6
docs/static/img/logo.svg vendored Normal file
View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" fill="none">
<rect width="100" height="100" rx="20" fill="#1e78ff"/>
<path d="M25 65 L40 70 L50 55 L60 40 L75 50 L75 65 L25 65Z" fill="white" opacity="0.9"/>
<path d="M25 65 L25 35 L50 55 L50 70Z" fill="white" opacity="0.6"/>
<path d="M50 55 L75 50 L60 40 L50 55Z" fill="white" opacity="0.75"/>
</svg>

After

Width:  |  Height:  |  Size: 372 B

6
docs/tsconfig.json Normal file
View File

@ -0,0 +1,6 @@
{
"extends": "@docusaurus/tsconfig",
"compilerOptions": {
"baseUrl": "."
}
}