Files
paint-ui/lib/text-utils.ts

157 lines
3.5 KiB
TypeScript
Raw Normal View History

feat: implement comprehensive text tool (Phase 11) Add complete text rendering system with the following features: **Text Tool Core:** - TextTool class with click-to-place text functionality - Text cursor and pointer event handling - Integration with DrawCommand for undo/redo support **Text Rendering:** - Multi-line text support with line height control - Custom letter spacing - Text alignment (left/center/right) - Font families: 13 web-safe fonts + 14 popular Google Fonts - Dynamic Google Font loading via Web Font Loader API - Font styles (normal/italic) and weights (100-900) **Text Dialog UI:** - Full-featured text editor dialog - Live preview of text with all formatting - Font family selection (web-safe + Google Fonts) - Font size (8-500px), style, and weight controls - Color picker with hex input - Text alignment options - Line height slider (0.5-3x) - Letter spacing slider (-10 to 50px) - Multi-line text input with textarea **State Management:** - text-store with Zustand + persist middleware - All text settings preserved across sessions - Dialog state management (open/close) - Click position tracking **Integration:** - Added text tool to tool palette with Type icon - Registered TextTool in canvas tool system - Added TextDialog to editor layout - Full type safety with TypeScript interfaces **Undoable:** - Text rendering fully integrated with command pattern - Each text insertion creates single undo point - Proper before/after state capture This completes Phase 11 of the implementation plan, marking the transition from MVP to a fully-featured image editor. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-21 09:45:05 +01:00
import type { TextSettings } from '@/types/text';
/**
* Render text on canvas with specified settings
*/
export function renderText(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
settings: TextSettings
): void {
if (!settings.text) return;
ctx.save();
// Build font string
const font = `${settings.fontStyle} ${settings.fontWeight} ${settings.fontSize}px ${settings.fontFamily}`;
ctx.font = font;
ctx.fillStyle = settings.color;
ctx.textAlign = settings.align;
ctx.textBaseline = settings.baseline;
// Handle multi-line text
const lines = settings.text.split('\n');
const lineHeightPx = settings.fontSize * settings.lineHeight;
lines.forEach((line, index) => {
const lineY = y + index * lineHeightPx;
// Apply letter spacing if needed
if (settings.letterSpacing !== 0) {
renderTextWithLetterSpacing(ctx, line, x, lineY, settings.letterSpacing);
} else {
ctx.fillText(line, x, lineY);
}
});
ctx.restore();
}
/**
* Render text with custom letter spacing
*/
function renderTextWithLetterSpacing(
ctx: CanvasRenderingContext2D,
text: string,
x: number,
y: number,
letterSpacing: number
): void {
const chars = text.split('');
let currentX = x;
// Adjust starting position based on text align
if (ctx.textAlign === 'center') {
const totalWidth = measureTextWidth(ctx, text, letterSpacing);
currentX = x - totalWidth / 2;
} else if (ctx.textAlign === 'right') {
const totalWidth = measureTextWidth(ctx, text, letterSpacing);
currentX = x - totalWidth;
}
// Draw each character with spacing
chars.forEach((char) => {
ctx.fillText(char, currentX, y);
currentX += ctx.measureText(char).width + letterSpacing;
});
}
/**
* Measure text width including letter spacing
*/
function measureTextWidth(
ctx: CanvasRenderingContext2D,
text: string,
letterSpacing: number
): number {
const chars = text.split('');
let width = 0;
chars.forEach((char) => {
width += ctx.measureText(char).width + letterSpacing;
});
return width - letterSpacing; // Remove last letter spacing
}
/**
* Common web-safe fonts
*/
export const WEB_SAFE_FONTS = [
'Arial',
'Arial Black',
'Comic Sans MS',
'Courier New',
'Georgia',
'Impact',
'Lucida Console',
'Lucida Sans Unicode',
'Palatino Linotype',
'Tahoma',
'Times New Roman',
'Trebuchet MS',
'Verdana',
] as const;
/**
* Popular Google Fonts (will be loaded dynamically)
*/
export const GOOGLE_FONTS = [
'Roboto',
'Open Sans',
'Lato',
'Montserrat',
'Oswald',
'Source Sans Pro',
'Raleway',
'PT Sans',
'Merriweather',
'Playfair Display',
'Ubuntu',
'Noto Sans',
'Poppins',
'Inter',
] as const;
/**
* Load a Google Font dynamically
*/
export function loadGoogleFont(fontFamily: string): Promise<void> {
return new Promise((resolve, reject) => {
// Check if font is already loaded
if (document.fonts.check(`16px "${fontFamily}"`)) {
resolve();
return;
}
// Create link element to load font
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = `https://fonts.googleapis.com/css2?family=${fontFamily.replace(/ /g, '+')}:wght@100;300;400;500;700;900&display=swap`;
link.onload = () => {
// Wait for font to be ready
document.fonts.load(`16px "${fontFamily}"`).then(() => {
resolve();
}).catch(reject);
};
link.onerror = () => {
reject(new Error(`Failed to load font: ${fontFamily}`));
};
document.head.appendChild(link);
});
}