The coloring canvas provides advanced interactive features including flood-fill coloring, drag-and-drop text editing, and touch-optimized controls:
import React, { useRef, useEffect, useState, useCallback } from 'react';
import { StoryPage, DraggableTextItem } from '../types';
const PALETTE_COLORS = [
'#FF5733', '#33FF57', '#3357FF', '#FF33A1',
'#A133FF', '#33FFA1', '#FFFF33', '#FF8C33',
'#8C33FF', '#33FFEC', '#EC33FF', '#FFC300',
'#000000', '#FFFFFF', '#C7C7C7', '#8B4513'
];
type Tool = 'brush' | 'text';
const ColoringCanvas: React.FC<{
page: StoryPage;
onSavePage: (pageData: Partial<StoryPage>) => void;
}> = ({ page, onSavePage }) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const [currentTool, setCurrentTool] = useState<Tool>('brush');
const [selectedColor, setSelectedColor] = useState(PALETTE_COLORS[0]);
const [textItems, setTextItems] = useState<DraggableTextItem[]>(page.textItems || []);
const floodFill = useCallback((
canvas: HTMLCanvasElement,
x: number,
y: number,
fillColor: string
) => {
const ctx = canvas.getContext('2d');
if (!ctx) return;
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
const targetColor = getColorAtPosition(data, x, y, canvas.width);
const fillColorRgb = hexToRgb(fillColor);
if (colorsEqual(targetColor, fillColorRgb)) return;
const stack = [{ x, y }];
const visited = new Set<string>();
while (stack.length > 0) {
const { x: currentX, y: currentY } = stack.pop()!;
const key = `${currentX},${currentY}`;
if (visited.has(key)) continue;
visited.add(key);
if (currentX < 0 || currentX >= canvas.width ||
currentY < 0 || currentY >= canvas.height) continue;
const currentColor = getColorAtPosition(data, currentX, currentY, canvas.width);
if (!colorsEqual(currentColor, targetColor)) continue;
setColorAtPosition(data, currentX, currentY, canvas.width, fillColorRgb);
stack.push(
{ x: currentX + 1, y: currentY },
{ x: currentX - 1, y: currentY },
{ x: currentX, y: currentY + 1 },
{ x: currentX, y: currentY - 1 }
);
}
ctx.putImageData(imageData, 0, 0);
saveCanvasState();
}, [selectedColor]);
const handleCanvasInteraction = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
const canvas = canvasRef.current;
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const x = Math.floor((e.clientX - rect.left) * scaleX);
const y = Math.floor((e.clientY - rect.top) * scaleY);
if (currentTool === 'brush') {
floodFill(canvas, x, y, selectedColor);
} else if (currentTool === 'text') {
addTextElement(x, y);
}
}, [currentTool, selectedColor, floodFill]);
const saveCanvasState = useCallback(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const coloredImageData = canvas.toDataURL('image/png');
onSavePage({
coloredImage: coloredImageData,
textItems: textItems
});
}, [textItems, onSavePage]);
return (
<div className="coloring-canvas-container">
<canvas
ref={canvasRef}
onClick={handleCanvasInteraction}
className="interactive-canvas"
/>
<div className="color-palette">
{PALETTE_COLORS.map(color => (
<button
key={color}
onClick={() => setSelectedColor(color)}
style={{ backgroundColor: color }}
className={selectedColor === color ? 'selected' : ''}
/>
))}
</div>
{textItems.map(item => (
<DraggableText
key={item.id}
item={item}
onUpdate={updateTextItem}
onDelete={() => deleteTextItem(item.id)}
/>
))}
</div>
);
};