10 lineHeight: number;
11 textAlign: 'left' | 'center' | 'right';
12 uploadedImage: HTMLImageElement | null;
13 imageUrl: string;
14 imagePosition: 'fit' | 'fill' | 'stretch' | 'tile';
15 imageBrightness: number;
16 imageContrast: number;
17 imageSaturation: number;
18 imageRotation: number;
19 imageFlipX: boolean;
20 imageFlipY: boolean;
21 imageBlur: number;
22 shadowEnabled: boolean;
23 shadowColor: string;
103 lineHeight: 1.2,
104 textAlign: 'center',
105 uploadedImage: null,
106 imageUrl: '',
107 imagePosition: 'fill',
108 imageBrightness: 100,
109 imageContrast: 100,
110 imageSaturation: 100,
111 imageRotation: 0,
112 imageFlipX: false,
113 imageFlipY: false,
114 imageBlur: 0,
115 shadowEnabled: false,
116 shadowColor: '#000000',
133 });
134
135 const [activeTab, setActiveTab] = useState<'text' | 'image' | 'effects' | 'export'>('text');
136 const [history, setHistory] = useState<HistoryState[]>([]);
137 const [historyIndex, setHistoryIndex] = useState(-1);
206 }
207
208 if (!state.uploadedImage && !state.gradientEnabled) return;
209
210 // Save context
268 });
269
270 // Apply image or gradient texture
271 ctx.globalCompositeOperation = state.blendMode as GlobalCompositeOperation;
272 ctx.globalAlpha = state.opacity / 100;
295 ctx.fillStyle = gradient;
296 ctx.fillRect(0, 0, canvas.width, canvas.height);
297 } else if (state.uploadedImage) {
298 // Apply image filters
299 ctx.filter = `brightness(${state.imageBrightness}%) contrast(${state.imageContrast}%) saturate(${state.imageSaturation}%) blur(${state.imageBlur}px)`;
300
301 // Calculate image dimensions and position
302 let { width: imgWidth, height: imgHeight } = state.uploadedImage;
303 let drawX = 0, drawY = 0, drawWidth = imgWidth, drawHeight = imgHeight;
304
307 ctx.translate(canvas.width / 2, canvas.height / 2);
308
309 if (state.imageRotation !== 0) {
310 ctx.rotate((state.imageRotation * Math.PI) / 180);
311 }
312
313 const scaleX = state.imageFlipX ? -1 : 1;
314 const scaleY = state.imageFlipY ? -1 : 1;
315 ctx.scale(scaleX, scaleY);
316
317 // Handle different image positioning modes
318 switch (state.imagePosition) {
319 case 'fit':
320 const fitScale = Math.min(canvas.width / imgWidth, canvas.height / imgHeight);
339 case 'tile':
340 // Create pattern for tiling
341 const pattern = ctx.createPattern(state.uploadedImage, 'repeat');
342 if (pattern) {
343 ctx.fillStyle = pattern;
349 }
350
351 ctx.drawImage(state.uploadedImage, drawX, drawY, drawWidth, drawHeight);
352 ctx.restore();
353 }
364 }, [updateCanvas]);
365
366 const handleImageUpload = (file: File) => {
367 if (!file.type.startsWith('image/')) {
368 alert('Please upload a valid image file');
369 return;
370 }
373 const reader = new FileReader();
374 reader.onload = (e) => {
375 const img = new Image();
376 img.onload = () => {
377 const newState = {
378 ...state,
379 uploadedImage: img,
380 imageUrl: e.target?.result as string
381 };
382 setState(newState);
385 };
386 img.onerror = () => {
387 alert('Failed to load image');
388 setIsLoading(false);
389 };
396 const file = e.target.files?.[0];
397 if (file) {
398 handleImageUpload(file);
399 }
400 };
405 const file = e.dataTransfer.files[0];
406 if (file) {
407 handleImageUpload(file);
408 }
409 };
425 };
426
427 const downloadImage = (format: 'png' | 'jpg' | 'svg' = 'png', scale: number = 1) => {
428 const canvas = canvasRef.current;
429 if (!canvas) return;
446 if (ctx) {
447 ctx.scale(scale, scale);
448 ctx.drawImage(canvas, 0, 0);
449 }
450 }
451
452 const link = document.createElement('a');
453 link.download = `image-text-studio.${format}`;
454 link.href = downloadCanvas.toDataURL(format === 'jpg' ? 'image/jpeg' : 'image/png', 0.9);
455 link.click();
456 };
457
458 const resetImage = () => {
459 const newState = {
460 ...state,
461 uploadedImage: null,
462 imageUrl: ''
463 };
464 setState(newState);
492 <div>
493 <h1 className="text-xl font-semibold">Text Studio</h1>
494 <p className="text-sm text-white/60">Professional Image Typography</p>
495 </div>
496 </div>
514 </button>
515 <button
516 onClick={() => downloadImage('png', 2)}
517 className="btn-primary"
518 disabled={!state.uploadedImage && !state.gradientEnabled}
519 >
520 Export
531 {/* Tab Navigation */}
532 <div className="tab-list">
533 {(['text', 'image', 'effects', 'export'] as const).map((tab) => (
534 <button
535 key={tab}
646 )}
647
648 {/* Image Controls */}
649 {activeTab === 'image' && (
650 <div className="tool-panel animate-fade-in">
651 <div className="tool-section">
652 <h3 className="text-lg font-semibold mb-4">Image Upload</h3>
653 {!state.uploadedImage ? (
654 <div
655 className={`upload-area p-8 text-center cursor-pointer ${
662 >
663 <div className="text-4xl mb-4">📸</div>
664 <p className="text-lg mb-2">Drop image or click to browse</p>
665 <p className="text-sm text-white/60">
666 PNG, JPG, GIF, WebP, SVG
669 ref={fileInputRef}
670 type="file"
671 accept="image/*"
672 onChange={handleFileSelect}
673 className="hidden"
678 <div className="relative">
679 <img
680 src={state.imageUrl}
681 alt="Uploaded"
682 className="w-full h-32 object-cover rounded-lg"
683 />
684 <button
685 onClick={resetImage}
686 className="absolute top-2 right-2 bg-red-500 hover:bg-red-600 text-white rounded-full w-8 h-8 flex items-center justify-center transition-colors"
687 >
693 className="btn-secondary w-full"
694 >
695 Change Image
696 </button>
697 <input
698 ref={fileInputRef}
699 type="file"
700 accept="image/*"
701 onChange={handleFileSelect}
702 className="hidden"
706 </div>
707
708 {state.uploadedImage && (
709 <>
710 <div className="tool-section">
714 <label className="block text-sm font-medium mb-2">Position Mode</label>
715 <select
716 value={state.imagePosition}
717 onChange={(e) => updateState({ imagePosition: e.target.value as any })}
718 className="form-control"
719 >
727 <div>
728 <label className="block text-sm font-medium mb-2">
729 Rotation: {state.imageRotation}°
730 </label>
731 <input
733 min="0"
734 max="360"
735 value={state.imageRotation}
736 onChange={(e) => updateState({ imageRotation: parseInt(e.target.value) })}
737 className="range-slider"
738 />
743 <input
744 type="checkbox"
745 checked={state.imageFlipX}
746 onChange={(e) => updateState({ imageFlipX: e.target.checked })}
747 className="rounded"
748 />
752 <input
753 type="checkbox"
754 checked={state.imageFlipY}
755 onChange={(e) => updateState({ imageFlipY: e.target.checked })}
756 className="rounded"
757 />
763
764 <div className="tool-section">
765 <h3 className="text-lg font-semibold mb-4">Image Adjustments</h3>
766 <div className="space-y-4">
767 <div>
768 <label className="block text-sm font-medium mb-2">
769 Brightness: {state.imageBrightness}%
770 </label>
771 <input
773 min="0"
774 max="200"
775 value={state.imageBrightness}
776 onChange={(e) => updateState({ imageBrightness: parseInt(e.target.value) })}
777 className="range-slider"
778 />
781 <div>
782 <label className="block text-sm font-medium mb-2">
783 Contrast: {state.imageContrast}%
784 </label>
785 <input
787 min="0"
788 max="200"
789 value={state.imageContrast}
790 onChange={(e) => updateState({ imageContrast: parseInt(e.target.value) })}
791 className="range-slider"
792 />
795 <div>
796 <label className="block text-sm font-medium mb-2">
797 Saturation: {state.imageSaturation}%
798 </label>
799 <input
801 min="0"
802 max="200"
803 value={state.imageSaturation}
804 onChange={(e) => updateState({ imageSaturation: parseInt(e.target.value) })}
805 className="range-slider"
806 />
809 <div>
810 <label className="block text-sm font-medium mb-2">
811 Blur: {state.imageBlur}px
812 </label>
813 <input
815 min="0"
816 max="20"
817 value={state.imageBlur}
818 onChange={(e) => updateState({ imageBlur: parseInt(e.target.value) })}
819 className="range-slider"
820 />
1131 <div className="space-y-3">
1132 <button
1133 onClick={() => downloadImage('png', 1)}
1134 className="btn-primary w-full"
1135 disabled={!state.uploadedImage && !state.gradientEnabled}
1136 >
1137 Download PNG (1x)
1138 </button>
1139 <button
1140 onClick={() => downloadImage('png', 2)}
1141 className="btn-secondary w-full"
1142 disabled={!state.uploadedImage && !state.gradientEnabled}
1143 >
1144 Download PNG (2x)
1145 </button>
1146 <button
1147 onClick={() => downloadImage('png', 3)}
1148 className="btn-secondary w-full"
1149 disabled={!state.uploadedImage && !state.gradientEnabled}
1150 >
1151 Download PNG (3x)
1152 </button>
1153 <button
1154 onClick={() => downloadImage('jpg', 2)}
1155 className="btn-secondary w-full"
1156 disabled={!state.uploadedImage && !state.gradientEnabled}
1157 >
1158 Download JPG (2x)
1175 </div>
1176
1177 {!state.uploadedImage && !state.gradientEnabled ? (
1178 <div className="canvas-container rounded-lg p-12 text-center text-white/60 min-h-[500px] flex items-center justify-center">
1179 <div className="animate-pulse">
1180 <div className="text-6xl mb-4">🎨</div>
1181 <p className="text-xl mb-2">Upload an image or enable gradient</p>
1182 <p className="text-sm">to see your text come to life</p>
1183 </div>
1203 {/* Floating Action Button */}
1204 <button
1205 onClick={() => downloadImage('png', 2)}
1206 className="fab"
1207 disabled={!state.uploadedImage && !state.gradientEnabled}
1208 data-tooltip="Quick Export"
1209 >