Drawing with React Native
I've been working on 575 (the app for the next generation of Haiku poets) for the last few months – it's my pride and joy. As a team, we love to experiment with light-hearted and non-traditional features that bring a lot of fun to the app.
One such feature is the ability to add your signature to the haikus you post. Think of it as an alternative to a username or profile picture - a bit a of throwback to the Pictochats of the Nintendo DS days.
But how would you implement this in React Native, our app framework of choice? The answer is to use PanResponder
.
Code Walkthrough
We will represent a "drawing" as a list of lines, and in turn represent a line as a list of points. A point is just an x and y coordinate:
export type Point = {
x: number;
y: number;
};
We of course want or input to be fully controlled - although we will keep track of the current stroke inside the component (currentPoints
).
type WhiteboardProps = {
strokes: Stroke[];
setStrokes: React.Dispatch<React.SetStateAction<Stroke[]>>;
};
const DrawInput = ({
strokes: previousStrokes,
setStrokes: setPreviousStrokes,
}: WhiteboardProps) => {
const [currentPoints, setCurrentPoints] = useState<Point[]>([]);
We'll create our PanResponder:
const panResponder = PanResponder.create({
onStartShouldSetPanResponder: (_) => true,
onMoveShouldSetPanResponder: (_) => true,
onPanResponderGrant: onTouch,
onPanResponderMove: onTouch,
onPanResponderRelease: () => onResponderRelease(),
});
When the user touches the screen, we'll add a point to our current line:
const onTouch = (evt: GestureResponderEvent) => {
setCurrentPoints([
...currentPoints,
{
x: evt.nativeEvent.locationX,
y: evt.nativeEvent.locationY
},
]);
};
When the user 'lets go' of the stroke, we can convert the current path into an svg, and add it to the controlling state.
const onResponderRelease = () => {
if (currentPoints.length < 1) return;
if (currentPoints.length === 1) {
let p = currentPoints[0];
let distance = Math.sqrt(strokeWidth) / 2;
currentPoints.push({ x: p.x + distance, y: p.y + distance });
}
let newElement: Stroke = {
type: "Path",
attributes: {
d: pointsToSvg(currentPoints),
},
};
setPreviousStrokes((oldPrevStrokes) => [...oldPrevStrokes, newElement]);
setCurrentPoints([]);
};
Finally, we return the drawn element as an svg, setting the outer View
to have the correct panHandlers
from our PanResponder
.
return (
<View style={{flex: 1}} {...panResponder.panHandlers}>
<Svg style={{flex: 1}}>
<G>
{previousStrokes.map((stroke) => (
<Path
{...stroke.attributes}
key={JSON.stringify(stroke.attributes)}
/>
))}
<Path
d={pointsToSvg(currentPoints)}
stroke="black"
strokeWidth={4}
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
/>
</G>
</Svg>
</View>
);
};
So how do we convert lists of points into SVG paths? The code is fairly simple. Say we have a list of points like this:
[{x: 0, y: 0}, {x: 10, y: 20}, {x: 99, y: 99}]
Our SVG Path Commands would look like this.
M 0 0 L 10 20 L 99 99
const pointsToSvg = (points: Point[]) => {
if (points.length > 0) {
return (
`M ${points[0].x},${points[0].y}` +
points.slice(1).map((point) => ` L ${point.x},${point.y}`)
);
} else {
return "";
}
};
Storing Drawings Efficiently
For 575 we use a no-SQL database, and store the signatures as SVGs. This is not the simplest setup, but it is the most convenient and simple. However, they do create ridiculously long SVGs:
<svg xmlns="http://www.w3.org/2000/svg" width="400" height="200" version="1.1">
<g>
<path d="M 76.66665649414062,108.66665649414062 L 75.33332824707031,110.99998474121094, L 75.33332824707031,112.33332824707031, L 76,110.99998474121094, L 78.33332824707031,106.99998474121094, L 84,98.33332824707031, L 91,87.66665649414062, L 97.66665649414062,77.66665649414062, L 103.33332824707031,70.66665649414062, L 114,63.666656494140625, ...
The solution here is pretty simple - just round the numbers! We can use number.toFixed(0)
to chop the decimal point off the end, reducing overall storage size by a factor of 10 with little to no degradation in quality at this scale.
You can find my finished code here. Happy drawing!