From react-native-3d
When and how to use React Native Skia for 2.5D graphics instead of full 3D. Covers Canvas setup, Path drawing, transforms, touch handling, and hybrid approaches.
How this skill is triggered — by the user, by Claude, or both
Slash command
/react-native-3d:skia-3d-alternativeThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
This skill covers when React Native Skia is a better choice than React Three Fiber, and how to implement 2.5D graphics with it.
This skill covers when React Native Skia is a better choice than React Three Fiber, and how to implement 2.5D graphics with it.
| Scenario | Recommendation |
|---|---|
| True 3D rotation (any axis) | R3F |
| Isometric/2.5D view (fixed angle) | Skia (simpler) |
| Vector graphics, charts, diagrams | Skia |
| Complex touch/gesture interactions | Skia (better integration) |
| Performance-critical 2D | Skia (GPU-accelerated 2D) |
| Wireframes with fixed camera | Either - Skia may be simpler |
| Physics-based 3D simulation | R3F |
| iOS Simulator reliability needed | Skia (more reliable) |
npm install @shopify/react-native-skia
import { Canvas, Path, Skia } from '@shopify/react-native-skia';
import { View, StyleSheet } from 'react-native';
function BasicSkiaScene() {
// Create a path
const path = Skia.Path.Make();
path.moveTo(50, 50);
path.lineTo(150, 50);
path.lineTo(150, 150);
path.lineTo(50, 150);
path.close();
return (
<View style={styles.container}>
<Canvas style={styles.canvas}>
<Path
path={path}
color="#4a90d9"
style="stroke"
strokeWidth={2}
/>
</Canvas>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
canvas: { flex: 1 },
});
For tensegrity-style structures with a fixed isometric view:
import { Canvas, Path, Circle, Skia, vec } from '@shopify/react-native-skia';
import { useMemo } from 'react';
// Isometric projection constants
const ISO_ANGLE = Math.PI / 6; // 30 degrees
const COS_A = Math.cos(ISO_ANGLE);
const SIN_A = Math.sin(ISO_ANGLE);
// Convert 3D point to 2D isometric
function toIsometric(x: number, y: number, z: number, centerX: number, centerY: number, scale: number) {
const isoX = (x - z) * COS_A * scale + centerX;
const isoY = (x + z) * SIN_A * scale - y * scale + centerY;
return { x: isoX, y: isoY };
}
interface Node3D {
id: string;
x: number;
y: number;
z: number;
}
interface Connection {
from: string;
to: string;
type: 'rod' | 'cable';
}
function IsometricStructure({
nodes,
connections,
width,
height,
scale = 50
}: {
nodes: Node3D[];
connections: Connection[];
width: number;
height: number;
scale?: number;
}) {
const centerX = width / 2;
const centerY = height / 2;
// Project all nodes to 2D
const projectedNodes = useMemo(() => {
const map = new Map<string, { x: number; y: number }>();
nodes.forEach(node => {
map.set(node.id, toIsometric(node.x, node.y, node.z, centerX, centerY, scale));
});
return map;
}, [nodes, centerX, centerY, scale]);
// Create paths for connections
const rodPath = useMemo(() => {
const path = Skia.Path.Make();
connections
.filter(c => c.type === 'rod')
.forEach(conn => {
const from = projectedNodes.get(conn.from);
const to = projectedNodes.get(conn.to);
if (from && to) {
path.moveTo(from.x, from.y);
path.lineTo(to.x, to.y);
}
});
return path;
}, [connections, projectedNodes]);
const cablePath = useMemo(() => {
const path = Skia.Path.Make();
connections
.filter(c => c.type === 'cable')
.forEach(conn => {
const from = projectedNodes.get(conn.from);
const to = projectedNodes.get(conn.to);
if (from && to) {
path.moveTo(from.x, from.y);
path.lineTo(to.x, to.y);
}
});
return path;
}, [connections, projectedNodes]);
return (
<Canvas style={{ width, height }}>
{/* Draw cables first (behind) */}
<Path
path={cablePath}
color="#4a90d9"
style="stroke"
strokeWidth={1}
/>
{/* Draw rods */}
<Path
path={rodPath}
color="#666666"
style="stroke"
strokeWidth={3}
/>
{/* Draw nodes on top */}
{nodes.map(node => {
const pos = projectedNodes.get(node.id);
if (!pos) return null;
return (
<Circle
key={node.id}
cx={pos.x}
cy={pos.y}
r={8}
color="#ffffff"
/>
);
})}
</Canvas>
);
}
Skia integrates cleanly with react-native-gesture-handler:
import { Canvas, Circle, useValue } from '@shopify/react-native-skia';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import { useSharedValue } from 'react-native-reanimated';
function TouchableNode({ initialX, initialY, radius = 20 }) {
const x = useSharedValue(initialX);
const y = useSharedValue(initialY);
const panGesture = Gesture.Pan()
.onUpdate((e) => {
x.value = e.absoluteX;
y.value = e.absoluteY;
});
return (
<GestureDetector gesture={panGesture}>
<Canvas style={{ flex: 1 }}>
<Circle cx={x} cy={y} r={radius} color="#ff6b6b" />
</Canvas>
</GestureDetector>
);
}
import { Canvas, Circle, Skia } from '@shopify/react-native-skia';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import { useState, useMemo } from 'react';
interface Node {
id: string;
x: number;
y: number;
}
function SelectableNodes({ nodes, onSelect }: { nodes: Node[]; onSelect: (id: string) => void }) {
const [selectedId, setSelectedId] = useState<string | null>(null);
const tapGesture = Gesture.Tap()
.onEnd((e) => {
// Find which node was tapped
const tapX = e.x;
const tapY = e.y;
const hitRadius = 25; // Touch target size
const hitNode = nodes.find(node => {
const dx = node.x - tapX;
const dy = node.y - tapY;
return Math.sqrt(dx * dx + dy * dy) < hitRadius;
});
if (hitNode) {
setSelectedId(hitNode.id);
onSelect(hitNode.id);
} else {
setSelectedId(null);
}
});
return (
<GestureDetector gesture={tapGesture}>
<Canvas style={{ flex: 1 }}>
{nodes.map(node => (
<Circle
key={node.id}
cx={node.x}
cy={node.y}
r={15}
color={selectedId === node.id ? '#ff6b6b' : '#4ecdc4'}
/>
))}
</Canvas>
</GestureDetector>
);
}
import { Canvas, Group, Circle, Path } from '@shopify/react-native-skia';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import { useSharedValue, useDerivedValue } from 'react-native-reanimated';
function ZoomableCanvas({ children }) {
const scale = useSharedValue(1);
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
const savedScale = useSharedValue(1);
const savedTranslateX = useSharedValue(0);
const savedTranslateY = useSharedValue(0);
const pinchGesture = Gesture.Pinch()
.onStart(() => {
savedScale.value = scale.value;
})
.onUpdate((e) => {
scale.value = Math.max(0.5, Math.min(3, savedScale.value * e.scale));
});
const panGesture = Gesture.Pan()
.onStart(() => {
savedTranslateX.value = translateX.value;
savedTranslateY.value = translateY.value;
})
.onUpdate((e) => {
translateX.value = savedTranslateX.value + e.translationX;
translateY.value = savedTranslateY.value + e.translationY;
});
const composed = Gesture.Simultaneous(pinchGesture, panGesture);
// Create transform matrix
const transform = useDerivedValue(() => [
{ translateX: translateX.value },
{ translateY: translateY.value },
{ scale: scale.value },
]);
return (
<GestureDetector gesture={composed}>
<Canvas style={{ flex: 1 }}>
<Group transform={transform}>
{children}
</Group>
</Canvas>
</GestureDetector>
);
}
import { Canvas, Circle, useSharedValueEffect, useValue } from '@shopify/react-native-skia';
import { useSharedValue, withSpring, withRepeat, withTiming } from 'react-native-reanimated';
import { useEffect } from 'react';
function PulsingNode({ x, y }) {
const radius = useSharedValue(15);
// Pulse animation
useEffect(() => {
radius.value = withRepeat(
withTiming(25, { duration: 500 }),
-1,
true
);
}, []);
// Bridge reanimated value to Skia
const skiaRadius = useValue(15);
useSharedValueEffect(() => {
skiaRadius.current = radius.value;
}, radius);
return (
<Circle cx={x} cy={y} r={skiaRadius} color="#ff6b6b" />
);
}
Use Skia for UI overlays on top of R3F 3D:
import { View, StyleSheet } from 'react-native';
import { Canvas as R3FCanvas } from '@react-three/fiber/native';
import { Canvas as SkiaCanvas, Circle, Text } from '@shopify/react-native-skia';
function HybridScene() {
return (
<View style={styles.container}>
{/* 3D layer */}
<R3FCanvas style={StyleSheet.absoluteFill}>
<ambientLight />
<mesh>
<boxGeometry />
<meshStandardMaterial color="orange" />
</mesh>
</R3FCanvas>
{/* 2D UI overlay */}
<SkiaCanvas style={StyleSheet.absoluteFill} pointerEvents="none">
<Circle cx={50} cy={50} r={20} color="rgba(255,0,0,0.5)" />
<Text
x={100}
y={50}
text="Score: 100"
font={null}
color="white"
/>
</SkiaCanvas>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
});
import { Canvas, Path, Skia } from '@shopify/react-native-skia';
function CurvedCable({ start, end, sag = 20 }) {
const path = useMemo(() => {
const p = Skia.Path.Make();
const midX = (start.x + end.x) / 2;
const midY = (start.y + end.y) / 2 + sag;
p.moveTo(start.x, start.y);
p.quadTo(midX, midY, end.x, end.y);
return p;
}, [start, end, sag]);
return (
<Path
path={path}
color="#4a90d9"
style="stroke"
strokeWidth={2}
strokeCap="round"
/>
);
}
function DashedLine({ start, end }) {
const path = useMemo(() => {
const p = Skia.Path.Make();
p.moveTo(start.x, start.y);
p.lineTo(end.x, end.y);
return p;
}, [start, end]);
return (
<Path
path={path}
color="#888888"
style="stroke"
strokeWidth={1}
strokeCap="round"
// Dash pattern: 5px dash, 5px gap
start={0}
end={1}
>
{/* Use PathEffect for dashes */}
</Path>
);
}
Consider switching to R3F if you need:
If you start with Skia and need to migrate:
// Skia version
<Path path={linePath} color="#fff" style="stroke" />
// R3F equivalent
<line>
<bufferGeometry>
<bufferAttribute
attach="attributes-position"
array={new Float32Array([x1, y1, z1, x2, y2, z2])}
count={2}
itemSize={3}
/>
</bufferGeometry>
<lineBasicMaterial color="#ffffff" />
</line>
| Metric | Skia | R3F |
|---|---|---|
| 2D drawing | Faster | Overkill |
| 100 nodes + lines | 60fps | 60fps |
| 1000 nodes + lines | 60fps | ~30fps (without instancing) |
| Touch responsiveness | Excellent | Good (raycast overhead) |
| iOS Simulator | Reliable | Unreliable |
| Memory usage | Lower | Higher |
| Setup complexity | Simple | More complex |
npx claudepluginhub smartwatermelon/smartwatermelon-marketplace --plugin react-native-3dBuild declarative 3D scenes with React Three Fiber in React apps using JSX components for Three.js objects. For interactive configurators, games, portfolios, and data viz.
Provides architecture patterns for integrating Three.js, GSAP ScrollTrigger, React Three Fiber, Framer Motion, and React Spring in complex 3D web experiences. Use for multi-library setups, scroll-driven animations, physics interactions, state management, and performance optimization.
Builds Three.js and React Three Fiber (R3F) 3D scenes: setup, animation, GLTF/GLB loading, physics with @react-three/rapier, WebGPU, Drei helpers, and performance optimization.