103 lines
4.5 KiB
TypeScript
103 lines
4.5 KiB
TypeScript
import React, { useState, useRef } from 'react';
|
|
import { View, Text, StyleSheet, TouchableOpacity, PanResponder, Alert } from 'react-native';
|
|
import { Canvas, Path, Skia } from '@shopify/react-native-skia';
|
|
|
|
export default function WhiteboardScreen() {
|
|
const [paths, setPaths] = useState<Array<{ d: string; color: string; width: number }>>([]);
|
|
const [color, setColor] = useState('#00A0C8');
|
|
const [strokeWidth, setStrokeWidth] = useState(3);
|
|
const currentPath = useRef('');
|
|
const drawing = useRef(false);
|
|
|
|
const COLORS = ['#00A0C8', '#ff4444', '#44bb44', '#ffbb00', '#bb44bb', '#fff'];
|
|
|
|
const panResponder = PanResponder.create({
|
|
onStartShouldSetPanResponder: () => true,
|
|
onPanResponderGrant: (e) => {
|
|
const { locationX, locationY } = e.nativeEvent;
|
|
currentPath.current = `M${locationX} ${locationY}`;
|
|
drawing.current = true;
|
|
},
|
|
onPanResponderMove: (e) => {
|
|
if (!drawing.current) return;
|
|
const { locationX, locationY } = e.nativeEvent;
|
|
currentPath.current += ` L${locationX} ${locationY}`;
|
|
setPaths(prev => {
|
|
const next = [...prev];
|
|
if (next.length && next[next.length - 1].d === 'drawing') {
|
|
next[next.length - 1] = { d: currentPath.current, color, width: strokeWidth };
|
|
} else {
|
|
next.push({ d: currentPath.current, color, width: strokeWidth });
|
|
}
|
|
return next;
|
|
});
|
|
},
|
|
onPanResponderRelease: () => { drawing.current = false; },
|
|
});
|
|
|
|
const clear = () => { setPaths([]); currentPath.current = ''; };
|
|
const undo = () => setPaths(prev => prev.slice(0, -1));
|
|
const share = () => Alert.alert('공유', 'SR 채팅방으로 화이트보드를 공유합니다');
|
|
|
|
return (
|
|
<View style={s.container}>
|
|
<View style={s.header}>
|
|
<Text style={s.title}>화이트보드</Text>
|
|
<View style={s.headerActions}>
|
|
<TouchableOpacity style={s.headerBtn} onPress={undo}><Text style={s.headerBtnText}>실행취소</Text></TouchableOpacity>
|
|
<TouchableOpacity style={s.headerBtn} onPress={clear}><Text style={s.headerBtnText}>지우기</Text></TouchableOpacity>
|
|
<TouchableOpacity style={[s.headerBtn, s.shareBtn]} onPress={share}><Text style={s.shareBtnText}>공유</Text></TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
|
|
<View style={s.canvas} {...panResponder.panHandlers}>
|
|
<Canvas style={{ flex: 1, backgroundColor: '#1A1F2E' }}>
|
|
{paths.map((p, i) => {
|
|
const skiaPath = Skia.Path.MakeFromSVGString(p.d);
|
|
if (!skiaPath) return null;
|
|
const paint = Skia.Paint();
|
|
paint.setColor(Skia.Color(p.color));
|
|
paint.setStrokeWidth(p.width);
|
|
paint.setStyle(1);
|
|
return <Path key={i} path={skiaPath} paint={paint} />;
|
|
})}
|
|
</Canvas>
|
|
</View>
|
|
|
|
<View style={s.toolbar}>
|
|
<View style={s.colorRow}>
|
|
{COLORS.map(c => (
|
|
<TouchableOpacity key={c} style={[s.colorBtn, { backgroundColor: c, borderWidth: color === c ? 2 : 0, borderColor: '#fff' }]} onPress={() => setColor(c)} />
|
|
))}
|
|
</View>
|
|
<View style={s.widthRow}>
|
|
{[2, 4, 8].map(w => (
|
|
<TouchableOpacity key={w} style={[s.widthBtn, strokeWidth === w && s.widthBtnActive]} onPress={() => setStrokeWidth(w)}>
|
|
<View style={[s.widthDot, { width: w * 4, height: w * 4, borderRadius: w * 2 }]} />
|
|
</TouchableOpacity>
|
|
))}
|
|
</View>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const s = StyleSheet.create({
|
|
container: { flex: 1, backgroundColor: '#0A0E1A' },
|
|
header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 12, borderBottomWidth: 1, borderBottomColor: '#333' },
|
|
title: { color: '#fff', fontSize: 18, fontWeight: '700' },
|
|
headerActions: { flexDirection: 'row', gap: 8 },
|
|
headerBtn: { paddingHorizontal: 12, paddingVertical: 6, backgroundColor: '#1A1F2E', borderRadius: 8 },
|
|
headerBtnText: { color: '#aaa', fontSize: 13 },
|
|
shareBtn: { backgroundColor: '#003366' },
|
|
shareBtnText: { color: '#fff', fontWeight: '700', fontSize: 13 },
|
|
canvas: { flex: 1 },
|
|
toolbar: { padding: 12, borderTopWidth: 1, borderTopColor: '#333', backgroundColor: '#0A0E1A' },
|
|
colorRow: { flexDirection: 'row', gap: 10, marginBottom: 10, justifyContent: 'center' },
|
|
colorBtn: { width: 28, height: 28, borderRadius: 14 },
|
|
widthRow: { flexDirection: 'row', gap: 16, justifyContent: 'center', alignItems: 'center' },
|
|
widthBtn: { padding: 8, borderRadius: 8 },
|
|
widthBtnActive: { backgroundColor: '#1A1F2E' },
|
|
widthDot: { backgroundColor: '#fff' },
|
|
});
|