Project_DrawingBoard.jsx
React.js/lessons/06_event_handling/projects/Project_DrawingBoard.jsx
/**
* Project: 互動式畫板 — 事件處理練習
*
* 學習重點:
* - 滑鼠事件(mouseDown, mouseUp, mouseMove, mouseLeave)
* - 事件物件的屬性(clientX, clientY, currentTarget)
* - getBoundingClientRect() 計算相對座標
* - 事件處理函式的寫法
*
* 使用方式:將此檔案內容複製到你的 App.jsx 中
*/
import { useState, useCallback } from 'react';
function DrawingBoard() {
const [isDrawing, setIsDrawing] = useState(false);
const [lines, setLines] = useState([]);
const [currentLine, setCurrentLine] = useState([]);
const [color, setColor] = useState('#4f46e5');
const [brushSize, setBrushSize] = useState(4);
const [tool, setTool] = useState('pen'); // 'pen' | 'eraser'
const [history, setHistory] = useState([]);
const colors = [
'#0f172a', '#ef4444', '#f59e0b', '#22c55e',
'#3b82f6', '#8b5cf6', '#ec4899', '#4f46e5',
];
const getPosition = useCallback((e) => {
const rect = e.currentTarget.getBoundingClientRect();
return {
x: e.clientX - rect.left,
y: e.clientY - rect.top,
};
}, []);
const handleMouseDown = (e) => {
setIsDrawing(true);
const pos = getPosition(e);
setCurrentLine([pos]);
};
const handleMouseMove = (e) => {
if (!isDrawing) return;
const pos = getPosition(e);
setCurrentLine((prev) => [...prev, pos]);
};
const handleMouseUp = () => {
if (!isDrawing) return;
setIsDrawing(false);
if (currentLine.length > 1) {
const newLine = {
points: currentLine,
color: tool === 'eraser' ? '#ffffff' : color,
size: tool === 'eraser' ? brushSize * 4 : brushSize,
};
setHistory((prev) => [...prev, lines]);
setLines((prev) => [...prev, newLine]);
}
setCurrentLine([]);
};
const handleMouseLeave = () => {
if (isDrawing) {
handleMouseUp();
}
};
const clearBoard = () => {
setHistory((prev) => [...prev, lines]);
setLines([]);
};
const undo = () => {
if (history.length === 0) return;
const prevState = history[history.length - 1];
setLines(prevState);
setHistory((prev) => prev.slice(0, -1));
};
const renderLine = (line, index) => {
if (line.points.length < 2) return null;
const pathData = line.points
.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x} ${p.y}`)
.join(' ');
return (
<path
key={index}
d={pathData}
stroke={line.color}
strokeWidth={line.size}
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
/>
);
};
const renderCurrentLine = () => {
if (currentLine.length < 2) return null;
const pathData = currentLine
.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x} ${p.y}`)
.join(' ');
return (
<path
d={pathData}
stroke={tool === 'eraser' ? '#ffffff' : color}
strokeWidth={tool === 'eraser' ? brushSize * 4 : brushSize}
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
/>
);
};
const toolbarStyle = {
display: 'flex',
alignItems: 'center',
gap: '16px',
padding: '12px 16px',
backgroundColor: '#f8fafc',
borderRadius: '12px',
marginBottom: '12px',
flexWrap: 'wrap',
};
return (
<div style={{
maxWidth: '700px',
margin: '32px auto',
fontFamily: "'Segoe UI', sans-serif",
padding: '0 16px',
}}>
<h1 style={{ textAlign: 'center', color: '#0f172a', marginBottom: '24px' }}>
互動式畫板
</h1>
{/* 工具列 */}
<div style={toolbarStyle}>
{/* 工具選擇 */}
<div style={{ display: 'flex', gap: '4px' }}>
{[
{ key: 'pen', icon: '✏️', label: '畫筆' },
{ key: 'eraser', icon: '🧹', label: '橡皮擦' },
].map((t) => (
<button
key={t.key}
onClick={() => setTool(t.key)}
title={t.label}
style={{
padding: '6px 12px',
borderRadius: '8px',
border: 'none',
backgroundColor: tool === t.key ? '#4f46e5' : 'white',
color: tool === t.key ? 'white' : '#64748b',
cursor: 'pointer',
fontSize: '0.9rem',
}}
>
{t.icon}
</button>
))}
</div>
{/* 分隔線 */}
<div style={{ width: '1px', height: '24px', backgroundColor: '#e2e8f0' }} />
{/* 顏色選擇 */}
<div style={{ display: 'flex', gap: '4px' }}>
{colors.map((c) => (
<div
key={c}
onClick={() => { setColor(c); setTool('pen'); }}
style={{
width: '24px',
height: '24px',
borderRadius: '50%',
backgroundColor: c,
cursor: 'pointer',
border: color === c && tool === 'pen' ? '3px solid #94a3b8' : '2px solid transparent',
boxSizing: 'border-box',
}}
/>
))}
</div>
<div style={{ width: '1px', height: '24px', backgroundColor: '#e2e8f0' }} />
{/* 筆刷大小 */}
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={{ fontSize: '0.8rem', color: '#94a3b8' }}>粗細</span>
<input
type="range"
min="1"
max="20"
value={brushSize}
onChange={(e) => setBrushSize(Number(e.target.value))}
style={{ width: '80px', accentColor: '#4f46e5' }}
/>
<span style={{ fontSize: '0.8rem', color: '#64748b', width: '24px' }}>{brushSize}</span>
</div>
<div style={{ width: '1px', height: '24px', backgroundColor: '#e2e8f0' }} />
{/* 操作按鈕 */}
<button
onClick={undo}
disabled={history.length === 0}
style={{
padding: '6px 12px',
borderRadius: '8px',
border: '1px solid #e2e8f0',
backgroundColor: 'white',
color: history.length === 0 ? '#cbd5e1' : '#64748b',
cursor: history.length === 0 ? 'not-allowed' : 'pointer',
fontSize: '0.85rem',
}}
>
↩ 復原
</button>
<button
onClick={clearBoard}
disabled={lines.length === 0}
style={{
padding: '6px 12px',
borderRadius: '8px',
border: '1px solid #fca5a5',
backgroundColor: 'white',
color: lines.length === 0 ? '#cbd5e1' : '#ef4444',
cursor: lines.length === 0 ? 'not-allowed' : 'pointer',
fontSize: '0.85rem',
}}
>
🗑 清除
</button>
</div>
{/* 畫布 */}
<svg
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseLeave}
style={{
width: '100%',
height: '450px',
backgroundColor: 'white',
borderRadius: '12px',
border: '2px solid #e2e8f0',
cursor: tool === 'eraser' ? 'cell' : 'crosshair',
display: 'block',
touchAction: 'none',
}}
>
{lines.map(renderLine)}
{renderCurrentLine()}
</svg>
<p style={{ textAlign: 'center', color: '#cbd5e1', fontSize: '0.8rem', marginTop: '8px' }}>
按住滑鼠左鍵拖曳繪畫 · 線條數:{lines.length} · 雙擊任何色塊選色
</p>
</div>
);
}
export default DrawingBoard;
関連記事
App.js
App.js — javascript source code from the React.js learning materials (React.js/lessons/01_introduction/example/src/App.js).
記事を読む →App.test.js
App.test.js — javascript source code from the React.js learning materials (React.js/lessons/01_introduction/example/src/App.test.js).
記事を読む →index.js
index.js — javascript source code from the React.js learning materials (React.js/lessons/01_introduction/example/src/index.js).
記事を読む →reportWebVitals.js
reportWebVitals.js — javascript source code from the React.js learning materials (React.js/lessons/01_introduction/example/src/reportWebVitals.js).
記事を読む →setupTests.js
setupTests.js — javascript source code from the React.js learning materials (React.js/lessons/01_introduction/example/src/setupTests.js).
記事を読む →Project_HelloReact.jsx
Project_HelloReact.jsx — javascript source code from the React.js learning materials (React.js/lessons/01_introduction/projects/Project_HelloReact.jsx).
記事を読む →