@dimacrossbow/target-zone-react-native
v0.1.0
Published
React Native (Expo) components for @dimacrossbow/target-zone-core — Skia-rendered targets, zones, shots, with pan / pinch / double-tap gestures.
Maintainers
Readme
@dimacrossbow/target-zone-react-native
React Native (Expo) renderer for @dimacrossbow/target-zone-core. Renders targets,
scoring zones and shots with @shopify/react-native-skia,
and drives the camera (pan / pinch / double-tap zoom) and tap events with
react-native-gesture-handler
PIXI is web/WebGL-only, so it can't run in React Native. This package reuses the
PIXI-free domain layer of core (@dimacrossbow/target-zone-core/domain — entities,
models, events) and re-implements only the rendering and input layers natively.
Status
Implemented: target backdrop image, zones (SVG paths with cut-out holes,
accurate hit-testing, and highlight fill), shots (image + color-tinted mask,
or geometry), decoration layers with Circle / Image / Text / Path
asset nodes, camera pan / pinch / double-tap-to-zoom with Maps-style fit + pan
clamp, and surface / zone / shot tap events.
This brings the renderer to feature parity with the web (@dimacrossbow/target-zone-vue)
build's component surface, in a data-driven form (you pass target + layers
data rather than declaring child components).
Known differences from the web build (paradigm, not gaps):
- No hover/pointer events on zones or assets — touch input is tap-based, so
there is no
pointer-over/pointer-out. Zone highlights are shown statically for any zone that declares ahighlight(toggle withhighlightZones). - No declarative child components (
<TargetLayer><CircleAsset/></TargetLayer>) — layers and assets are passed as plain data (see below). TextNodehonors a common subset of PIXI'sTextStyle(fontFamily,fontSize,fill,fontWeight,fontStyle).- Undo/redo history is app-level (drive the core entities yourself), as on web.
Install
npm install @dimacrossbow/target-zone-react-native @dimacrossbow/target-zone-coreThen install the native peer dependencies in your app:
npx expo install @shopify/react-native-skia react-native-gesture-handler react-native-reanimatedAdd reanimated's Babel plugin last in babel.config.js:
plugins: ['react-native-reanimated/plugin'];Skia + reanimated + gesture-handler require a custom dev client (not Expo
Go): npx expo run:android / run:ios, or build a dev client with EAS.
Usage
Render under a GestureHandlerRootView at the app root:
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { TargetCanvas, type ITargetData } from '@dimacrossbow/target-zone-react-native';
const target: ITargetData = {
x: 0, y: 0, width: 320, height: 320,
image: 'https://example.com/target.png', // or require('./assets/target.png')
zones: [
{ name: 'bullseye', geometry: 'M 160,160 m -38,0 a 38,38 0 1,0 76,0 a 38,38 0 1,0 -76,0', data: { score: 10 } },
],
shots: [],
};
export default function App() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<TargetCanvas
target={target}
width={360}
height={640}
doubleTapZoom={2}
onSurfaceTap={(point, zone) => {
// point is target-local; zone is the zone under the tap (or null)
}}
onShotTap={(shot) => {/* inspect / remove */}}
onZoneTap={(zone) => {/* … */}}
/>
</GestureHandlerRootView>
);
}A runnable Expo demo lives in ../react-native-example.
Props
| Prop | Default | Notes |
| --- | --- | --- |
| target | — | ITargetData or a Target entity. Memoize it to avoid rebuilds. |
| width / height | — | Canvas size in px. |
| backgroundColor | '#202020' | Color behind the world. |
| minZoom / maxZoom | 0.1 / 8 | Zoom clamp. |
| doubleTapZoom | 2 | Double-tap zoom factor toward the tapped point; <= 1 disables. |
| fit | true | object-fit: contain to the target rect (auto fit + pan clamp). |
| debugZones | false | Draw zone shapes in a debug color (otherwise hit-test only). |
| highlightZones | true | Fill zones that declare a highlight with their configured color. |
| layers | [] | Decoration layers (Circle/Image/Text/Path assets). See below. |
| onSurfaceTap / onZoneTap / onShotTap / onCameraChange | — | Event callbacks. |
Decoration layers
Pass layers to render Circle / Image / Text / Path assets in
target-local mm space — the data-driven counterpart of @dimacrossbow/target-zone-vue's
<TargetLayer> + asset components. Colors are numeric 0xRRGGBB, as in core/vue.
import { TargetCanvas, type ILayerData } from '@dimacrossbow/target-zone-react-native';
const layers: ILayerData[] = [
{
name: 'hull',
zIndex: -1, // < 0 → behind the target backdrop; >= 0 → in front
assets: [
{ kind: 'path', geometry: 'M 10,10 L 90,20 L 50,80 Z',
fill: { color: 0x33aaff, alpha: 0.15 }, stroke: { color: 0x33aaff, width: 2 } },
],
},
{
name: 'markers',
zIndex: 1000,
assets: [
{ kind: 'circle', x: 160, y: 160, radius: 4, color: 0xffffff,
stroke: { color: 0x000000, width: 1 } },
{ kind: 'text', x: 8, y: 8, text: 'Shots: 3', anchor: { x: 0, y: 0 },
style: { fontSize: 16, fill: 0xffffff, fontWeight: 'bold' } },
{ kind: 'image', x: 300, y: 8, image: 'https://…/logo.png',
width: 48, height: 48, anchor: { x: 1, y: 0 } },
],
},
];
<TargetCanvas target={target} layers={layers} width={360} height={640} />Each asset node shares x / y / rotation / alpha / visible and maps to a
core/vue primitive:
| kind | vue component | Key fields |
| --- | --- | --- |
| circle | CircleAsset | radius, color, stroke? |
| image | ImageAsset | image, width?, height?, anchor? |
| text | TextAsset | text, style?, anchor? |
| path | PathAsset | geometry, holes?, fill?, stroke? |
The node components (CircleNode, ImageNode, TextNode, PathNode,
AssetNode, LayerNode) and the colorToCss helper are exported for custom
renderers.
Event semantics mirror the web build: a shot tap fires onShotTap only; a
tap inside a zone fires onZoneTap and onSurfaceTap (with the zone); a
tap on empty surface fires onSurfaceTap with zone = null.
Notes
- This package ships TypeScript source (no build step) so the consuming
app's Babel handles the reanimated worklets.
@dimacrossbow/target-zone-core/domainis consumed as a prebuilt PIXI-free bundle. pixi.jsis a dev-only dependency, present purely so core's.d.tstypes resolve. It is never bundled into your app.
License
MIT © dimacrossbow
