|
3 | 3 | X, Crop, Pencil, Type, Save, |
4 | 4 | ChevronRight, Undo2, Redo2, |
5 | 5 | Square, Circle, ArrowUpRight, Eraser, |
6 | | - SlidersHorizontal, Check, Maximize2 |
| 6 | + SlidersHorizontal, Check, Maximize2, |
| 7 | + Bold, Italic, Underline, AlignLeft, AlignCenter, AlignRight, |
| 8 | + Type as TypeIcon |
7 | 9 | } from 'lucide-react'; |
8 | 10 |
|
9 | 11 | interface ImageEditorModalProps { |
@@ -35,6 +37,11 @@ export const ImageEditorModal: React.FC<ImageEditorModalProps> = ({ src, onSave, |
35 | 37 | const [isAddingText, setIsAddingText] = useState(false); |
36 | 38 | const [textPos, setTextPos] = useState({ x: 0, y: 0 }); |
37 | 39 | const [fontFamily, setFontFamily] = useState('Inter, sans-serif'); |
| 40 | + const [fontSize, setFontSize] = useState(24); |
| 41 | + const [isBold, setIsBold] = useState(false); |
| 42 | + const [isItalic, setIsItalic] = useState(false); |
| 43 | + const [isUnderline, setIsUnderline] = useState(false); |
| 44 | + const [textAlign, setTextAlign] = useState<CanvasTextAlign>('left'); |
38 | 45 |
|
39 | 46 | // Adjustments State |
40 | 47 | const [brightness, setBrightness] = useState(100); |
@@ -308,14 +315,38 @@ export const ImageEditorModal: React.FC<ImageEditorModalProps> = ({ src, onSave, |
308 | 315 | }; |
309 | 316 |
|
310 | 317 | const finalizeText = () => { |
311 | | - if (!textInput.trim() || !contextRef.current) { |
312 | | - setIsAddingText(false); |
313 | | - return; |
314 | | - } |
315 | 318 | const ctx = contextRef.current; |
316 | | - ctx.font = `bold ${brushSize * 4}px ${fontFamily}`; |
| 319 | + |
| 320 | + // Construct font string: "bold italic 24px Arial" |
| 321 | + const styleString = [ |
| 322 | + isBold ? 'bold' : '', |
| 323 | + isItalic ? 'italic' : '', |
| 324 | + `${fontSize}px`, |
| 325 | + fontFamily |
| 326 | + ].filter(Boolean).join(' '); |
| 327 | + |
| 328 | + ctx.font = styleString; |
317 | 329 | ctx.fillStyle = brushColor; |
| 330 | + ctx.textAlign = textAlign; |
| 331 | + |
318 | 332 | ctx.fillText(textInput, textPos.x, textPos.y); |
| 333 | + |
| 334 | + if (isUnderline) { |
| 335 | + const metrics = ctx.measureText(textInput); |
| 336 | + const textWidth = metrics.width; |
| 337 | + let startX = textPos.x; |
| 338 | + |
| 339 | + if (textAlign === 'center') startX = textPos.x - textWidth / 2; |
| 340 | + else if (textAlign === 'right') startX = textPos.x - textWidth; |
| 341 | + |
| 342 | + ctx.beginPath(); |
| 343 | + ctx.lineWidth = Math.max(1, fontSize / 15); |
| 344 | + ctx.strokeStyle = brushColor; |
| 345 | + ctx.moveTo(startX, textPos.y + fontSize / 4); |
| 346 | + ctx.lineTo(startX + textWidth, textPos.y + fontSize / 4); |
| 347 | + ctx.stroke(); |
| 348 | + } |
| 349 | + |
319 | 350 | setIsAddingText(false); |
320 | 351 | setTextInput(''); |
321 | 352 | saveHistory(); |
@@ -515,24 +546,52 @@ export const ImageEditorModal: React.FC<ImageEditorModalProps> = ({ src, onSave, |
515 | 546 | /> |
516 | 547 |
|
517 | 548 | {isAddingText && ( |
518 | | - <div className="absolute z-50 flex flex-col gap-2 p-3 bg-[#121212] border border-white/10 rounded-xl shadow-2xl" style={{ left: textPos.x, top: textPos.y }}> |
519 | | - <input |
520 | | - autoFocus |
521 | | - type="text" |
522 | | - value={textInput} |
523 | | - onChange={(e) => setTextInput(e.target.value)} |
524 | | - onKeyDown={(e) => { |
525 | | - e.stopPropagation(); |
526 | | - if (e.key === 'Enter') finalizeText(); |
527 | | - }} |
528 | | - onKeyUp={(e) => e.stopPropagation()} |
529 | | - onKeyPress={(e) => e.stopPropagation()} |
530 | | - className="bg-transparent border-b border-blue-500 text-white outline-none py-1 min-w-[150px]" |
531 | | - placeholder="Type labels..." |
532 | | - /> |
533 | | - <div className="flex justify-end gap-2"> |
534 | | - <button onClick={() => setIsAddingText(false)} className="p-1 hover:text-red-400"><X className="w-3 h-3"/></button> |
535 | | - <button onClick={finalizeText} className="p-1 hover:text-green-400"><Check className="w-3 h-3"/></button> |
| 549 | + <div className="absolute z-50 flex flex-col gap-3 p-4 bg-[#121212] border border-white/10 rounded-2xl shadow-2xl animate-in zoom-in duration-200" style={{ left: textPos.x, top: textPos.y - 120 }}> |
| 550 | + {/* Floating Style Toolbar */} |
| 551 | + <div className="flex items-center gap-1 border-b border-white/5 pb-2 mb-1"> |
| 552 | + <TextStyleButton active={isBold} onClick={() => setIsBold(!isBold)} icon={<Bold className="w-3.5 h-3.5" />} /> |
| 553 | + <TextStyleButton active={isItalic} onClick={() => setIsItalic(!isItalic)} icon={<Italic className="w-3.5 h-3.5" />} /> |
| 554 | + <TextStyleButton active={isUnderline} onClick={() => setIsUnderline(!isUnderline)} icon={<Underline className="w-3.5 h-3.5" />} /> |
| 555 | + <div className="w-px h-4 bg-white/10 mx-1" /> |
| 556 | + <TextStyleButton active={textAlign === 'left'} onClick={() => setTextAlign('left')} icon={<AlignLeft className="w-3.5 h-3.5" />} /> |
| 557 | + <TextStyleButton active={textAlign === 'center'} onClick={() => setTextAlign('center')} icon={<AlignCenter className="w-3.5 h-3.5" />} /> |
| 558 | + <TextStyleButton active={textAlign === 'right'} onClick={() => setTextAlign('right')} icon={<AlignRight className="w-3.5 h-3.5" />} /> |
| 559 | + <div className="w-px h-4 bg-white/10 mx-1" /> |
| 560 | + <div className="flex items-center gap-2 px-2"> |
| 561 | + <span className="text-[10px] font-bold text-gray-500">Size</span> |
| 562 | + <input |
| 563 | + type="number" |
| 564 | + value={fontSize} |
| 565 | + onChange={(e) => setFontSize(parseInt(e.target.value))} |
| 566 | + className="w-10 bg-white/5 border border-white/10 rounded px-1 py-0.5 text-[10px] text-white outline-none focus:border-blue-500" |
| 567 | + /> |
| 568 | + </div> |
| 569 | + </div> |
| 570 | + |
| 571 | + <div className="flex flex-col gap-2"> |
| 572 | + <input |
| 573 | + autoFocus |
| 574 | + type="text" |
| 575 | + value={textInput} |
| 576 | + onChange={(e) => setTextInput(e.target.value)} |
| 577 | + onKeyDown={(e) => { |
| 578 | + e.stopPropagation(); |
| 579 | + if (e.key === 'Enter') finalizeText(); |
| 580 | + if (e.key === 'Escape') setIsAddingText(false); |
| 581 | + }} |
| 582 | + onKeyUp={(e) => e.stopPropagation()} |
| 583 | + onKeyPress={(e) => e.stopPropagation()} |
| 584 | + className="bg-transparent border-b border-blue-500 text-white outline-none py-1 min-w-[200px] text-lg" |
| 585 | + placeholder="Type labels..." |
| 586 | + style={{ fontFamily, fontWeight: isBold ? 'bold' : 'normal', fontStyle: isItalic ? 'italic' : 'normal', textDecoration: isUnderline ? 'underline' : 'none', textAlign }} |
| 587 | + /> |
| 588 | + <div className="flex justify-between items-center gap-2"> |
| 589 | + <span className="text-[10px] text-gray-500 font-medium">Hit Enter to place</span> |
| 590 | + <div className="flex gap-2"> |
| 591 | + <button onClick={() => setIsAddingText(false)} className="px-3 py-1.5 bg-white/5 hover:bg-white/10 text-gray-400 rounded-lg text-xs transition-colors">Cancel</button> |
| 592 | + <button onClick={finalizeText} className="px-3 py-1.5 bg-blue-600 hover:bg-blue-500 text-white rounded-lg text-xs font-bold transition-colors">Place Text</button> |
| 593 | + </div> |
| 594 | + </div> |
536 | 595 | </div> |
537 | 596 | </div> |
538 | 597 | )} |
@@ -684,6 +743,16 @@ export const ImageEditorModal: React.FC<ImageEditorModalProps> = ({ src, onSave, |
684 | 743 | ); |
685 | 744 | }; |
686 | 745 |
|
| 746 | +const TextStyleButton: React.FC<{ active: boolean; onClick: () => void; icon: React.ReactNode }> = ({ active, onClick, icon }) => ( |
| 747 | + <button |
| 748 | + onClick={(e) => { e.stopPropagation(); onClick(); }} |
| 749 | + onMouseDown={(e) => e.stopPropagation()} |
| 750 | + className={`p-1.5 rounded-lg transition-all ${active ? 'bg-blue-600 text-white shadow-lg' : 'text-gray-500 hover:text-white hover:bg-white/5'}`} |
| 751 | + > |
| 752 | + {icon} |
| 753 | + </button> |
| 754 | +); |
| 755 | + |
687 | 756 | const ToolbarDivider = () => <div className="w-10 h-px bg-white/5 my-2 mx-auto" />; |
688 | 757 |
|
689 | 758 | const ToolButton: React.FC<{ active?: boolean; onClick: () => void; icon?: React.ReactNode; label?: string; disabled?: boolean; title?: string; children?: React.ReactNode }> = ({ active, onClick, icon, label, disabled, title, children }) => ( |
|
0 commit comments