feat(text-tool): implement Photoshop-style on-canvas text editor

- Replaced modal dialog with inline on-canvas text editor
- Text objects stored as editable entities (non-rasterized)
- Live preview with transparent textarea overlay
- Click on existing text to re-edit
- Drag transform handles to move text
- Auto-commit on click outside (via overlay)
- Text selection with visible highlight
- Hidden original text during editing to prevent double vision
- Position alignment fixes for editing existing text
- Keyboard shortcuts: Ctrl+Enter to commit, Escape to cancel

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-21 14:31:47 +01:00
parent 4e6dc6cb14
commit 69a468141c
7 changed files with 628 additions and 23 deletions

View File

@@ -4,8 +4,10 @@ import { useEffect, useRef, useState } from 'react';
import { useCanvasStore, useLayerStore, useToolStore } from '@/store';
import { useHistoryStore } from '@/store/history-store';
import { useSelectionStore } from '@/store/selection-store';
import { useTextStore } from '@/store/text-store';
import { drawMarchingAnts } from '@/lib/selection-utils';
import { getContext, drawGrid, drawCheckerboard } from '@/lib/canvas-utils';
import { renderText } from '@/lib/text-utils';
import { DrawCommand } from '@/core/commands';
import {
PencilTool,
@@ -25,6 +27,7 @@ import {
} from '@/tools';
import type { PointerState } from '@/types';
import { cn } from '@/lib/utils';
import { OnCanvasTextEditor } from './on-canvas-text-editor';
// Tool instances
const tools: Record<string, BaseTool> = {
@@ -66,6 +69,7 @@ export function CanvasWithTools() {
const { activeTool, settings } = useToolStore();
const { executeCommand } = useHistoryStore();
const { activeSelection, selectionType, isMarching } = useSelectionStore();
const { textObjects, editingTextId } = useTextStore();
const [marchingOffset, setMarchingOffset] = useState(0);
const [isPanning, setIsPanning] = useState(false);
@@ -129,6 +133,27 @@ export function CanvasWithTools() {
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = 'source-over';
// Draw text objects (skip the one being edited)
textObjects.forEach((textObj) => {
// Don't render text that's currently being edited
if (editingTextId && textObj.id === editingTextId) {
return;
}
renderText(ctx, textObj.x, textObj.y, {
text: textObj.text,
fontFamily: textObj.fontFamily,
fontSize: textObj.fontSize,
fontStyle: textObj.fontStyle,
fontWeight: textObj.fontWeight,
color: textObj.color,
align: textObj.align,
baseline: textObj.baseline,
lineHeight: textObj.lineHeight,
letterSpacing: textObj.letterSpacing,
});
});
// Draw grid if enabled (only within canvas bounds)
if (showGrid) {
drawGrid(ctx, gridSize, 'rgba(0, 0, 0, 0.15)', width, height);
@@ -141,7 +166,7 @@ export function CanvasWithTools() {
// Restore context state
ctx.restore();
}, [layers, width, height, zoom, offsetX, offsetY, showGrid, gridSize, backgroundColor, selection, pointer, activeSelection, isMarching, marchingOffset]);
}, [layers, width, height, zoom, offsetX, offsetY, showGrid, gridSize, backgroundColor, selection, pointer, activeSelection, isMarching, marchingOffset, textObjects, editingTextId]);
// Marching ants animation
useEffect(() => {
@@ -353,6 +378,9 @@ export function CanvasWithTools() {
ref={canvasRef}
className="absolute inset-0"
/>
{/* On-canvas text editor */}
<OnCanvasTextEditor />
</div>
);
}