|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Advanced Thumbnail Creator</title> |
|
<script src="https://cdn.tailwindcss.com"></script> |
|
<script src="https://kit.fontawesome.com/a076d05399.js" crossorigin="anonymous"></script> |
|
<style> |
|
@font-face { |
|
font-family: 'Pyidaungsu'; |
|
src: url('https://github.com/myanmartools/pyidaungsu-font/raw/master/Pyidaungsu.ttf') format('truetype'); |
|
} |
|
|
|
.myanmar-text { |
|
font-family: 'Pyidaungsu', sans-serif; |
|
} |
|
|
|
.thumbnail-container { |
|
position: relative; |
|
width: 100%; |
|
max-width: 1800px; |
|
margin: 0 auto; |
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); |
|
} |
|
|
|
.thumbnail-canvas { |
|
width: 100%; |
|
height: auto; |
|
border: 2px solid #e5e7eb; |
|
display: block; |
|
transition: all 0.3s ease; |
|
} |
|
|
|
.text-tool { |
|
position: absolute; |
|
cursor: move; |
|
user-select: none; |
|
} |
|
|
|
.text-tool.active { |
|
outline: 2px dashed #3b82f6; |
|
} |
|
|
|
.text-tool-handle { |
|
position: absolute; |
|
width: 10px; |
|
height: 10px; |
|
background: #3b82f6; |
|
bottom: -5px; |
|
right: -5px; |
|
cursor: nwse-resize; |
|
border-radius: 50%; |
|
} |
|
|
|
.color-picker { |
|
width: 30px; |
|
height: 30px; |
|
border: 2px solid #fff; |
|
box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); |
|
cursor: pointer; |
|
border-radius: 4px; |
|
} |
|
|
|
.tool-panel { |
|
transition: all 0.3s ease; |
|
} |
|
|
|
.tool-panel.collapsed { |
|
transform: translateX(100%); |
|
opacity: 0; |
|
pointer-events: none; |
|
} |
|
|
|
|
|
.gradient-overlay { |
|
position: absolute; |
|
top: 0; |
|
left: 0; |
|
width: 100%; |
|
height: 100%; |
|
pointer-events: none; |
|
} |
|
|
|
|
|
::-webkit-scrollbar { |
|
width: 8px; |
|
} |
|
|
|
::-webkit-scrollbar-track { |
|
background: #f1f1f1; |
|
border-radius: 4px; |
|
} |
|
|
|
::-webkit-scrollbar-thumb { |
|
background: #888; |
|
border-radius: 4px; |
|
} |
|
|
|
::-webkit-scrollbar-thumb:hover { |
|
background: #555; |
|
} |
|
|
|
|
|
.text-stroke { |
|
text-shadow: -1px -1px 0 #000, |
|
1px -1px 0 #000, |
|
-1px 1px 0 #000, |
|
1px 1px 0 #000; |
|
} |
|
|
|
|
|
.multi-gradient-overlay { |
|
position: absolute; |
|
top: 0; |
|
left: 0; |
|
width: 100%; |
|
height: 100%; |
|
pointer-events: none; |
|
background: linear-gradient(rgba(0, 0, 0, 0.3) 0%, |
|
rgba(0, 0, 0, 0.1) 20%, |
|
rgba(0, 0, 0, 0) 40%, |
|
rgba(0, 0, 0, 0) 60%, |
|
rgba(0, 0, 0, 0.1) 80%, |
|
rgba(0, 0, 0, 0.3) 100%); |
|
} |
|
|
|
|
|
.drag-handle { |
|
width: 20px; |
|
height: 20px; |
|
background: #3b82f6; |
|
border-radius: 50%; |
|
position: absolute; |
|
bottom: -10px; |
|
right: -10px; |
|
cursor: nwse-resize; |
|
z-index: 10; |
|
} |
|
|
|
|
|
.rotate-handle { |
|
width: 20px; |
|
height: 20px; |
|
background: #3b82f6; |
|
border-radius: 50%; |
|
position: absolute; |
|
top: -10px; |
|
right: -10px; |
|
cursor: grab; |
|
z-index: 10; |
|
} |
|
|
|
|
|
.scale-handle { |
|
width: 20px; |
|
height: 20px; |
|
background: #3b82f6; |
|
border-radius: 50%; |
|
position: absolute; |
|
bottom: -10px; |
|
left: -10px; |
|
cursor: nesw-resize; |
|
z-index: 10; |
|
} |
|
|
|
|
|
.tooltip { |
|
position: relative; |
|
display: inline-block; |
|
} |
|
|
|
.tooltip .tooltiptext { |
|
visibility: hidden; |
|
width: 120px; |
|
background-color: #333; |
|
color: #fff; |
|
text-align: center; |
|
border-radius: 6px; |
|
padding: 5px; |
|
position: absolute; |
|
z-index: 1; |
|
bottom: 125%; |
|
left: 50%; |
|
margin-left: -60px; |
|
opacity: 0; |
|
transition: opacity 0.3s; |
|
font-size: 12px; |
|
} |
|
|
|
.tooltip:hover .tooltiptext { |
|
visibility: visible; |
|
opacity: 1; |
|
} |
|
|
|
|
|
.btn-animate { |
|
transition: all 0.2s ease; |
|
transform: translateY(0); |
|
} |
|
|
|
.btn-animate:hover { |
|
transform: translateY(-2px); |
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); |
|
} |
|
|
|
|
|
input[type="range"] { |
|
-webkit-appearance: none; |
|
height: 6px; |
|
background: #e5e7eb; |
|
border-radius: 3px; |
|
} |
|
|
|
input[type="range"]::-webkit-slider-thumb { |
|
-webkit-appearance: none; |
|
appearance: none; |
|
width: 16px; |
|
height: 16px; |
|
background: #3b82f6; |
|
border-radius: 50%; |
|
cursor: pointer; |
|
} |
|
|
|
|
|
.file-upload { |
|
position: relative; |
|
overflow: hidden; |
|
} |
|
|
|
.file-upload input { |
|
position: absolute; |
|
top: 0; |
|
right: 0; |
|
bottom: 0; |
|
left: 0; |
|
width: 100%; |
|
height: 100%; |
|
opacity: 0; |
|
cursor: pointer; |
|
} |
|
|
|
|
|
.panel-transition { |
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); |
|
} |
|
|
|
|
|
.spinner { |
|
animation: spin 1s linear infinite; |
|
} |
|
|
|
@keyframes spin { |
|
0% { |
|
transform: rotate(0deg); |
|
} |
|
|
|
100% { |
|
transform: rotate(360deg); |
|
} |
|
} |
|
|
|
|
|
.transform-controls { |
|
position: absolute; |
|
display: flex; |
|
flex-direction: column; |
|
background: rgba(255, 255, 255, 0.9); |
|
border-radius: 4px; |
|
padding: 5px; |
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); |
|
z-index: 20; |
|
} |
|
|
|
.transform-btn { |
|
width: 30px; |
|
height: 30px; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
margin: 2px; |
|
border-radius: 4px; |
|
background: #f1f1f1; |
|
cursor: pointer; |
|
} |
|
|
|
.transform-btn:hover { |
|
background: #e5e5e5; |
|
} |
|
|
|
|
|
.text-edit-input { |
|
position: absolute; |
|
background: transparent; |
|
border: none; |
|
outline: none; |
|
resize: none; |
|
overflow: hidden; |
|
font-family: 'Pyidaungsu', sans-serif; |
|
padding: 0; |
|
margin: 0; |
|
line-height: 1.2; |
|
} |
|
|
|
|
|
.ai-loading-overlay { |
|
position: absolute; |
|
top: 0; |
|
left: 0; |
|
width: 100%; |
|
height: 100%; |
|
background: rgba(0, 0, 0, 0.5); |
|
display: flex; |
|
flex-direction: column; |
|
align-items: center; |
|
justify-content: center; |
|
color: white; |
|
z-index: 100; |
|
} |
|
</style> |
|
</head> |
|
|
|
<body class="bg-gray-50 min-h-screen"> |
|
<div class="container mx-auto px-4 py-8"> |
|
<div class="text-center mb-8"> |
|
<h1 class="text-3xl font-bold text-blue-600 mb-2">Advanced Thumbnail Creator</h1> |
|
<p class="text-gray-600">Create stunning thumbnails with AI enhancement and custom fonts</p> |
|
</div> |
|
|
|
<div class="bg-white rounded-xl shadow-lg overflow-hidden mb-8"> |
|
<div class="flex flex-col lg:flex-row gap-6"> |
|
|
|
<div class="w-full lg:w-1/3 p-6 bg-gray-50"> |
|
|
|
<div class="bg-white rounded-lg shadow-sm p-4 mb-6"> |
|
<h2 class="text-lg font-semibold mb-3 text-gray-800 flex items-center"> |
|
<i class="fas fa-cloud-upload-alt text-blue-500 mr-2"></i> |
|
Upload Image |
|
</h2> |
|
<div |
|
class="file-upload flex flex-col items-center justify-center border-2 border-dashed border-gray-300 rounded-lg p-6 bg-gray-50 hover:bg-gray-100 transition-colors"> |
|
<input type="file" id="imageUpload" accept="image/*" class="hidden"> |
|
<label for="imageUpload" |
|
class="cursor-pointer flex flex-col items-center"> |
|
<i class="fas fa-image text-4xl text-blue-400 mb-2"></i> |
|
<span class="text-gray-600 font-medium">Choose an image</span> |
|
<span class="text-sm text-gray-400 mt-1">or drag and drop</span> |
|
</label> |
|
</div> |
|
<div class="mt-3 grid grid-cols-3 gap-2"> |
|
<button id="removeImageBtn" |
|
class="py-2 bg-red-50 text-red-600 rounded hover:bg-red-100 disabled:opacity-50 transition-colors" |
|
disabled> |
|
<i class="fas fa-trash mr-1"></i> Remove |
|
</button> |
|
<button id="fitImageBtn" |
|
class="py-2 bg-blue-50 text-blue-600 rounded hover:bg-blue-100 disabled:opacity-50 transition-colors" |
|
disabled> |
|
<i class="fas fa-expand mr-1"></i> Fit |
|
</button> |
|
<button id="aiEnhanceBtn" |
|
class="py-2 bg-purple-50 text-purple-600 rounded hover:bg-purple-100 disabled:opacity-50 transition-colors" |
|
disabled> |
|
<i class="fas fa-magic mr-1"></i> AI Enhance |
|
</button> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="bg-white rounded-lg shadow-sm p-4 mb-6"> |
|
<h2 class="text-lg font-semibold mb-3 text-gray-800 flex items-center"> |
|
<i class="fas fa-font text-blue-500 mr-2"></i> |
|
Custom Font |
|
</h2> |
|
<div |
|
class="file-upload flex flex-col items-center justify-center border-2 border-dashed border-gray-300 rounded-lg p-4 bg-gray-50 hover:bg-gray-100 transition-colors"> |
|
<input type="file" id="fontUpload" accept=".ttf,.otf,.woff,.woff2" class="hidden"> |
|
<label for="fontUpload" |
|
class="cursor-pointer flex flex-col items-center"> |
|
<i class="fas fa-file-upload text-2xl text-blue-400 mb-1"></i> |
|
<span class="text-sm text-gray-600 font-medium">Upload Font</span> |
|
<span class="text-xs text-gray-400 mt-1">.ttf, .otf, .woff</span> |
|
</label> |
|
</div> |
|
<div id="fontLoadingIndicator" class="hidden text-center mt-2 text-sm text-gray-600 flex items-center justify-center"> |
|
<i class="fas fa-spinner spinner mr-2"></i> Loading font... |
|
</div> |
|
<div id="fontOptions" class="mt-3"> |
|
<div class="flex items-center mb-2 text-gray-600 text-sm hidden" |
|
id="loadedFontNameContainer"> |
|
<i class="fas fa-check-circle text-green-500 mr-2"></i> |
|
Custom font loaded: <span id="loadedFontName" class="font-medium ml-1"></span> |
|
</div> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">Font Family</label> |
|
<select id="fontFamily" |
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition"> |
|
<option value="Pyidaungsu">Pyidaungsu (Default)</option> |
|
<option value="Arial">Arial</option> |
|
<option value="Helvetica">Helvetica</option> |
|
<option value="Times New Roman">Times New Roman</option> |
|
<option value="Courier New">Courier New</option> |
|
<option value="Georgia">Georgia</option> |
|
<option value="Verdana">Verdana</option> |
|
</select> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="bg-white rounded-lg shadow-sm p-4 mb-6"> |
|
<h2 class="text-lg font-semibold mb-3 text-gray-800 flex items-center"> |
|
<i class="fas fa-font text-blue-500 mr-2"></i> |
|
Text Tools |
|
</h2> |
|
<div class="space-y-4"> |
|
<div> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">Text Content</label> |
|
<textarea id="myanmarText" |
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg myanmar-text focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition" |
|
placeholder="Enter your text here" rows="3"></textarea> |
|
</div> |
|
|
|
<div class="grid grid-cols-2 gap-4"> |
|
<div> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">Font Size</label> |
|
<input type="range" id="fontSize" min="10" max="100" value="24" class="w-full"> |
|
<div class="text-center text-sm text-gray-600 mt-1"><span |
|
id="fontSizeValue">24</span>px</div> |
|
</div> |
|
|
|
<div> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">Font Color</label> |
|
<div class="flex items-center gap-2"> |
|
<input type="color" id="fontColor" value="#ffffff" |
|
class="w-10 h-10 rounded cursor-pointer"> |
|
<button id="randomColorBtn" |
|
class="px-2 py-1 bg-gray-100 rounded hover:bg-gray-200 text-sm"> |
|
<i class="fas fa-random text-gray-600"></i> |
|
</button> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="grid grid-cols-2 gap-4"> |
|
<div> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">Background</label> |
|
<div class="flex items-center gap-2"> |
|
<input type="color" id="bgColor" value="#00000000" |
|
class="w-10 h-10 rounded cursor-pointer"> |
|
<button id="transparentBgBtn" |
|
class="px-2 py-1 bg-gray-100 rounded hover:bg-gray-200 text-sm"> |
|
Clear |
|
</button> |
|
</div> |
|
</div> |
|
|
|
<div> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">Opacity</label> |
|
<input type="range" id="textOpacity" min="0" max="100" value="100" class="w-full"> |
|
<div class="text-center text-sm text-gray-600 mt-1"><span |
|
id="opacityValue">100</span>%</div> |
|
</div> |
|
</div> |
|
|
|
<div> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">Font Style</label> |
|
<div class="flex space-x-2"> |
|
<button id="boldBtn" class="px-3 py-1 bg-gray-100 rounded hover:bg-gray-200 tooltip"> |
|
<i class="fas fa-bold"></i> |
|
<span class="tooltiptext">Bold</span> |
|
</button> |
|
<button id="italicBtn" class="px-3 py-1 bg-gray-100 rounded hover:bg-gray-200 tooltip"> |
|
<i class="fas fa-italic"></i> |
|
<span class="tooltiptext">Italic</span> |
|
</button> |
|
<button id="underlineBtn" class="px-3 py-1 bg-gray-100 rounded hover:bg-gray-200 tooltip"> |
|
<i class="fas fa-underline"></i> |
|
<span class="tooltiptext">Underline</span> |
|
</button> |
|
</div> |
|
</div> |
|
|
|
<div> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">Text Alignment</label> |
|
<div class="flex space-x-2"> |
|
<button id="alignLeftBtn" |
|
class="px-3 py-1 bg-blue-100 text-blue-600 rounded hover:bg-blue-200 tooltip"> |
|
<i class="fas fa-align-left"></i> |
|
<span class="tooltiptext">Align Left</span> |
|
</button> |
|
<button id="alignCenterBtn" |
|
class="px-3 py-1 bg-gray-100 rounded hover:bg-gray-200 tooltip"> |
|
<i class="fas fa-align-center"></i> |
|
<span class="tooltiptext">Align Center</span> |
|
</button> |
|
<button id="alignRightBtn" |
|
class="px-3 py-1 bg-gray-100 rounded hover:bg-gray-200 tooltip"> |
|
<i class="fas fa-align-right"></i> |
|
<span class="tooltipttext">Align Right</span> |
|
</button> |
|
</div> |
|
</div> |
|
|
|
<div class="space-y-3"> |
|
<div class="flex items-center justify-between"> |
|
<label class="block text-sm font-medium text-gray-700">Text Effects</label> |
|
<button id="toggleEffectsBtn" class="text-xs text-blue-500 hover:text-blue-600"> |
|
<i class="fas fa-chevron-down mr-1"></i> Show |
|
</button> |
|
</div> |
|
|
|
<div id="effectsPanel" class="hidden space-y-3"> |
|
<div class="grid grid-cols-2 gap-4"> |
|
<div> |
|
<div class="flex items-center mb-1"> |
|
<input type="checkbox" id="outlineCheck" class="h-4 w-4 mr-2"> |
|
<label for="outlineCheck" class="text-sm">Outline</label> |
|
</div> |
|
<div class="flex items-center space-x-2 pl-6"> |
|
<input type="color" id="outlineColor" value="#000000" class="w-8 h-8" |
|
disabled> |
|
<input type="range" id="outlineWidth" min="0" max="10" value="2" |
|
class="w-full" disabled> |
|
</div> |
|
<div class="pl-6 mt-2"> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">Outline |
|
Style</label> |
|
<select id="outlineStyle" |
|
class="px-2 py-1 border border-gray-300 rounded text-sm" |
|
disabled> |
|
<option value="solid">Solid</option> |
|
<option value="dashed">Dashed</option> |
|
<option value="dotted">Dotted</option> |
|
</select> |
|
</div> |
|
</div> |
|
<div> |
|
<div class="flex items-center mb-1"> |
|
<input type="checkbox" id="shadowCheck" class="h-4 w-4 mr-2"> |
|
<label for="shadowCheck" class="text-sm">Shadow</label> |
|
</div> |
|
<div class="flex items-center space-x-2 pl-6"> |
|
<label class="text-xs text-gray-600">Color</label> |
|
<input type="color" id="shadowColor" value="#000000" class="w-8 h-8" |
|
disabled> |
|
</div> |
|
<div class="pl-6 mt-2 grid grid-cols-2 gap-4"> |
|
<div> |
|
<label class="block text-xs font-medium text-gray-700 mb-1">Blur |
|
Radius</label> |
|
<input type="range" id="shadowBlur" min="0" max="20" value="5" |
|
class="w-full" disabled> |
|
</div> |
|
<div> |
|
<label class="block text-xs font-medium text-gray-700 mb-1">Offset |
|
(X, Y)</label> |
|
<div class="flex space-x-1"><input type="number" id="shadowOffsetX" |
|
placeholder="X" |
|
class="w-10 px-1 py-0.5 border rounded text-xs" |
|
disabled><input type="number" id="shadowOffsetY" |
|
placeholder="Y" |
|
class="w-10 px-1 py-0.5 border rounded text-xs" |
|
disabled></div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">Max Width</label> |
|
<input type="range" id="maxWidth" min="100" max="1800" value="800" |
|
class="w-full"> |
|
<div class="text-center text-sm text-gray-600 mt-1"><span |
|
id="maxWidthValue">800</span>px</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<button id="addTextBtn" |
|
class="w-full py-2.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 btn-animate flex items-center justify-center"> |
|
<i class="fas fa-plus mr-2"></i> Add Text to Thumbnail |
|
</button> |
|
<button id="updateTextBtn" |
|
class="w-full py-2.5 bg-green-600 text-white rounded-lg hover:bg-green-700 btn-animate flex items-center justify-center hidden"> |
|
<i class="fas fa-save mr-2"></i> Update Text |
|
</button> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="bg-white rounded-lg shadow-sm p-4 mb-6"> |
|
<h2 class="text-lg font-semibold mb-3 text-gray-800 flex items-center"> |
|
<i class="fas fa-layer-group text-blue-500 mr-2"></i> |
|
Overlay Effects |
|
</h2> |
|
<div class="space-y-4"> |
|
<div> |
|
<div class="flex items-center mb-1"> |
|
<input type="checkbox" id="gradientCheck" class="h-4 w-4 mr-2"> |
|
<label for="gradientCheck" class="text-sm">Gradient Overlay</label> |
|
</div> |
|
<div class="flex items-center space-x-2 pl-6"> |
|
<select id="gradientType" |
|
class="px-2 py-1 border border-gray-300 rounded text-sm" disabled> |
|
<option value="top">Top to Bottom</option> |
|
<option value="bottom">Bottom to Top</option> |
|
<option value="left">Left to Right</option> |
|
<option value="right">Right to Left</option> |
|
<option value="vignette">Vignette</option> |
|
<option value="multi">Multiple Vertical</option> |
|
</select> |
|
</div> |
|
</div> |
|
<div class="pl-6 grid grid-cols-2 gap-4"> |
|
<div> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">Color 1</label> |
|
<input type="color" id="gradientColor1" value="#000000" |
|
class="w-10 h-10 rounded cursor-pointer" disabled> |
|
</div> |
|
<div> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">Color 2</label> |
|
<input type="color" id="gradientColor2" value="#000000" |
|
class="w-10 h-10 rounded cursor-pointer" disabled> |
|
</div> |
|
</div> |
|
<div class="pl-6"> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">Gradient Opacity</label> |
|
<input type="range" id="gradientOpacity" min="0" max="100", value="50" |
|
class="w-full" disabled> |
|
<div class="text-center text-sm text-gray-600 mt-1"><span |
|
id="gradientOpacityValue">50</span>%</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="w-full lg:w-2/3 p-6"> |
|
<div class="bg-white rounded-lg shadow-sm p-4 mb-6"> |
|
<div class="flex justify-between items-center mb-4"> |
|
<h2 class="text-lg font-semibold text-gray-800 flex items-center"> |
|
<i class="fas fa-image text-blue-500 mr-2"></i> |
|
Thumbnail Preview |
|
</h2> |
|
<div class="flex items-center space-x-2"> |
|
<button id="clearAllBtn" |
|
class="px-3 py-1 bg-gray-100 rounded hover:bg-gray-200 text-sm disabled:opacity-50" |
|
disabled> |
|
<i class="fas fa-trash-alt mr-1"></i> Clear All |
|
</button> |
|
<button id="downloadBtn" |
|
class="px-3 py-1 bg-green-500 text-white rounded hover:bg-green-600 btn-animate disabled:opacity-50" |
|
disabled> |
|
<i class="fas fa-download mr-1"></i> Download |
|
</button> |
|
|
|
<button id="undoBtn" |
|
class="px-3 py-1 bg-gray-100 rounded hover:bg-gray-200 text-sm disabled:opacity-50" |
|
disabled> |
|
<i class="fas fa-undo mr-1"></i> Undo |
|
</button> |
|
<button id="redoBtn" |
|
class="px-3 py-1 bg-gray-100 rounded hover:bg-gray-200 text-sm disabled:opacity-50" |
|
disabled> |
|
<i class="fas fa-redo mr-1"></i> Redo |
|
|
|
</button> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="flex items-center justify-center space-x-2 mb-4 hidden" id="reorderControls"> |
|
<span class="text-sm text-gray-700">Layer:</span> |
|
<button id="bringForwardBtn" |
|
class="px-2 py-1 bg-gray-100 rounded hover:bg-gray-200 text-sm tooltip disabled:opacity-50" |
|
disabled> |
|
<i class="fas fa-arrow-up"></i> |
|
<span class="tooltiptext">Bring Forward</span> |
|
</button> |
|
<button id="sendBackwardBtn" |
|
class="px-2 py-1 bg-gray-100 rounded hover:bg-gray-200 text-sm tooltip disabled:opacity-50" |
|
disabled> |
|
<i class="fas fa-arrow-down"></i> |
|
<span class="tooltiptext">Send Backward</span> |
|
</button> |
|
<button id="bringToFrontBtn" |
|
class="px-2 py-1 bg-gray-100 rounded hover:bg-gray-200 text-sm tooltip disabled:opacity-50" |
|
disabled> |
|
<i class="fas fa-angle-double-up"></i> |
|
<span class="tooltiptext">Bring to Front</span> |
|
</button> |
|
<button id="sendToBackBtn" |
|
class="px-2 py-1 bg-gray-100 rounded hover:bg-gray-200 text-sm tooltip disabled:opacity-50" |
|
disabled> |
|
<i class="fas fa-angle-double-down"></i> |
|
<span class="tooltiptext">Send to Back</span> |
|
</button> |
|
</div> |
|
</div> |
|
|
|
<div class="thumbnail-container"> |
|
<canvas id="thumbnailCanvas" class="thumbnail-canvas" width="800" height="450"></canvas> |
|
<div id="aiLoadingOverlay" class="ai-loading-overlay hidden"> |
|
<i class="fas fa-spinner spinner text-4xl mb-2"></i> |
|
<p>Enhancing image with AI...</p> |
|
</div> |
|
</div> |
|
|
|
<div class="mt-4 flex flex-col sm:flex-row gap-4 items-center"> |
|
<div class="w-full sm:w-auto"> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">Thumbnail Size</label> |
|
<select id="thumbnailSize" |
|
class="w-full px-3 py-2 border border-gray-300 rounded focus:ring-blue-500 focus:border-blue-500"> |
|
<option value="800x450">800x450 (16:9)</option> |
|
<option value="1280x720">1280x720 (HD)</option> |
|
<option value="1920x1080">1920x1080 (Full HD)</option> |
|
<option value="custom">Custom</option> |
|
</select> |
|
</div> |
|
|
|
<div id="customSizeContainer" class="hidden w-full sm:w-auto"> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">Custom Size</label> |
|
<div class="flex items-center space-x-2"> |
|
<input type="number" id="customWidth" placeholder="Width" |
|
class="w-20 px-2 py-1 border border-gray-300 rounded" min="100" max="4000"> |
|
<span class="text-gray-500">×</span> |
|
<input type="number" id="customHeight" placeholder="Height" |
|
class="w-20 px-2 py-1 border border-gray-300 rounded" min="100" max="4000"> |
|
</div> |
|
</div> |
|
|
|
<button id="applySizeBtn" |
|
class="w-full sm:w-auto px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 btn-animate mt-6 sm:mt-0"> |
|
<i class="fas fa-check mr-1"></i> Apply Size |
|
</button> |
|
</div> |
|
</div> |
|
|
|
<div class="bg-white rounded-lg shadow-sm p-4"> |
|
<h2 class="text-lg font-semibold mb-3 text-gray-800 flex items-center"> |
|
<i class="fas fa-list text-blue-500 mr-2"></i> |
|
Text Elements |
|
</h2> |
|
<div class="space-y-2 max-h-60 overflow-y-auto" id="textElementsList"> |
|
<div class="text-center py-4 text-gray-500"> |
|
<i class="fas fa-info-circle mr-1"></i> No text elements added yet |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="text-center text-gray-500 text-sm mt-8"> |
|
<p>© 2023 Advanced Thumbnail Creator | Made with <i class="fas fa-heart text-red-400"></i></p> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
|
|
const canvas = document.getElementById('thumbnailCanvas'); |
|
const ctx = canvas.getContext('2d'); |
|
const imageUpload = document.getElementById('imageUpload'); |
|
const removeImageBtn = document.getElementById('removeImageBtn'); |
|
const fitImageBtn = document.getElementById('fitImageBtn'); |
|
const aiEnhanceBtn = document.getElementById('aiEnhanceBtn'); |
|
const aiLoadingOverlay = document.getElementById('aiLoadingOverlay'); |
|
const downloadBtn = document.getElementById('downloadBtn'); |
|
const clearAllBtn = document.getElementById('clearAllBtn'); |
|
const myanmarText = document.getElementById('myanmarText'); |
|
const fontSize = document.getElementById('fontSize'); |
|
const fontSizeValue = document.getElementById('fontSizeValue'); |
|
const fontColor = document.getElementById('fontColor'); |
|
const randomColorBtn = document.getElementById('randomColorBtn'); |
|
const bgColor = document.getElementById('bgColor'); |
|
const transparentBgBtn = document.getElementById('transparentBgBtn'); |
|
const textOpacity = document.getElementById('textOpacity'); |
|
const opacityValue = document.getElementById('opacityValue'); |
|
const addTextBtn = document.getElementById('addTextBtn'); |
|
const updateTextBtn = document.getElementById('updateTextBtn'); |
|
const boldBtn = document.getElementById('boldBtn'); |
|
const italicBtn = document.getElementById('italicBtn'); |
|
const underlineBtn = document.getElementById('underlineBtn'); |
|
const alignLeftBtn = document.getElementById('alignLeftBtn'); |
|
const alignCenterBtn = document.getElementById('alignCenterBtn'); |
|
const alignRightBtn = document.getElementById('alignRightBtn'); |
|
const thumbnailSize = document.getElementById('thumbnailSize'); |
|
const customSizeContainer = document.getElementById('customSizeContainer'); |
|
const customWidth = document.getElementById('customWidth'); |
|
const customHeight = document.getElementById('customHeight'); |
|
const applySizeBtn = document.getElementById('applySizeBtn'); |
|
const outlineCheck = document.getElementById('outlineCheck'); |
|
const outlineColor = document.getElementById('outlineColor'); |
|
const outlineWidth = document.getElementById('outlineWidth'); |
|
const outlineStyle = document.getElementById('outlineStyle'); |
|
const shadowBlur = document.getElementById('shadowBlur'); |
|
const shadowOffsetX = document.getElementById('shadowOffsetX'); |
|
const shadowOffsetY = document.getElementById('shadowOffsetY'); |
|
const shadowCheck = document.getElementById('shadowCheck'); |
|
const shadowColor = document.getElementById('shadowColor'); |
|
const maxWidth = document.getElementById('maxWidth'); |
|
const maxWidthValue = document.getElementById('maxWidthValue'); |
|
const gradientCheck = document.getElementById('gradientCheck'); |
|
const gradientType = document.getElementById('gradientType'); |
|
const gradientColor1 = document.getElementById('gradientColor1'); |
|
const gradientColor2 = document.getElementById('gradientColor2'); |
|
const gradientOpacity = document.getElementById('gradientOpacity'); |
|
const gradientOpacityValue = document.getElementById('gradientOpacityValue'); |
|
const textElementsList = document.getElementById('textElementsList'); |
|
const toggleEffectsBtn = document.getElementById('toggleEffectsBtn'); |
|
const effectsPanel = document.getElementById('effectsPanel'); |
|
const fontUpload = document.getElementById('fontUpload'); |
|
const undoBtn = document.getElementById('undoBtn'); |
|
const redoBtn = document.getElementById('redoBtn'); |
|
const fontFamily = document.getElementById('fontFamily'); |
|
const fontLoadingIndicator = document.getElementById('fontLoadingIndicator'); |
|
const fontOptions = document.getElementById('fontOptions'); |
|
const loadedFontNameContainer = document.getElementById('loadedFontNameContainer'); |
|
const reorderControls = document.getElementById('reorderControls'); |
|
const bringForwardBtn = document.getElementById('bringForwardBtn'); |
|
const sendBackwardBtn = document.getElementById('sendBackwardBtn'); |
|
const bringToFrontBtn = document.getElementById('bringToFrontBtn'); |
|
|
|
let uploadedImage = null; |
|
let activeTextElement = null; |
|
let textElements = []; |
|
let isDragging = false; |
|
let isResizing = false; |
|
let isRotating = false; |
|
let isScaling = false; |
|
let isManipulating = false; |
|
let startX, startY; |
|
let currentFontStyle = { |
|
bold: false, |
|
italic: false, |
|
underline: false, |
|
align: 'left', |
|
outline: false, |
|
outlineColor: '#000000', |
|
outlineWidth: 2, |
|
outlineStyle: 'solid', |
|
shadowBlur: 5, |
|
shadowOffset: { |
|
x: 3, |
|
y: 3 |
|
}, |
|
shadow: false, |
|
shadowColor: '#000000', |
|
maxWidth: 800, |
|
fontFamily: 'Pyidaungsu' |
|
}; |
|
let textEditInput = null; |
|
let customFonts = {}; |
|
|
|
let history = []; |
|
let historyIndex = -1; |
|
|
|
function initCanvas() { |
|
ctx.fillStyle = 'white'; |
|
ctx.fillRect(0, 0, canvas.width, canvas.height); |
|
ctx.fillStyle = '#94a3b8'; |
|
ctx.font = '20px Arial'; |
|
ctx.textAlign = 'center'; |
|
ctx.fillText('Upload an image to start designing', canvas.width / 2, canvas.height / 2); |
|
|
|
ctx.strokeStyle = '#e2e8f0'; |
|
ctx.lineWidth = 1; |
|
|
|
for (let x = 0; x <= canvas.width; x += 20) { |
|
ctx.beginPath(); |
|
ctx.moveTo(x, 0); |
|
ctx.lineTo(x, canvas.height); |
|
ctx.stroke(); |
|
} |
|
|
|
for (let y = 0; y <= canvas.width; y += 20) { |
|
ctx.beginPath(); |
|
ctx.moveTo(0, y); |
|
ctx.lineTo(canvas.width, canvas.height); |
|
ctx.stroke(); |
|
} |
|
} |
|
initCanvas(); |
|
|
|
imageUpload.addEventListener('change', handleImageUpload); |
|
removeImageBtn.addEventListener('click', removeImage); |
|
fitImageBtn.addEventListener('click', fitImageToCanvas); |
|
aiEnhanceBtn.addEventListener('click', enhanceImageWithAI); |
|
downloadBtn.addEventListener('click', downloadThumbnail); |
|
clearAllBtn.addEventListener('click', clearAllElements); |
|
fontSize.addEventListener('input', updateFontSizeValue); |
|
textOpacity.addEventListener('input', updateOpacityValue); |
|
addTextBtn.addEventListener('click', addTextToCanvas); |
|
updateTextBtn.addEventListener('click', updateTextElement); |
|
boldBtn.addEventListener('click', toggleBold); |
|
italicBtn.addEventListener('click', toggleItalic); |
|
underlineBtn.addEventListener('click', toggleUnderline); |
|
alignLeftBtn.addEventListener('click', () => setTextAlign('left')); |
|
alignCenterBtn.addEventListener('click', () => setTextAlign('center')); |
|
alignRightBtn.addEventListener('click', () => setTextAlign('right')); |
|
thumbnailSize.addEventListener('change', handleThumbnailSizeChange); |
|
applySizeBtn.addEventListener('click', applyCanvasSize); |
|
outlineCheck.addEventListener('change', toggleOutline); |
|
outlineColor.addEventListener('input', updateOutlineColor); |
|
outlineWidth.addEventListener('input', updateOutlineWidth); |
|
outlineStyle.addEventListener('change', updateOutlineStyle); |
|
shadowBlur.addEventListener('input', updateShadowBlur); |
|
shadowOffsetX.addEventListener('input', updateShadowOffset); |
|
shadowOffsetY.addEventListener('input', updateShadowOffset); |
|
shadowCheck.addEventListener('change', toggleShadow); |
|
shadowColor.addEventListener('input', updateShadowColor); |
|
maxWidth.addEventListener('input', updateMaxWidth); |
|
gradientCheck.addEventListener('change', toggleGradient); |
|
gradientType.addEventListener('change', redrawCanvas); |
|
gradientColor1.addEventListener('input', redrawCanvas); |
|
gradientColor2.addEventListener('input', redrawCanvas); |
|
gradientOpacity.addEventListener('input', updateGradientOpacity); |
|
undoBtn.addEventListener('click', undo); |
|
redoBtn.addEventListener('click', redo); |
|
randomColorBtn.addEventListener('click', generateRandomColor); |
|
bringForwardBtn.addEventListener('click', () => reorderTextElement('forward')); |
|
sendBackwardBtn.addEventListener('click', () => reorderTextElement('backward')); |
|
bringToFrontBtn.addEventListener('click', () => reorderTextElement('front')); |
|
sendToBackBtn.addEventListener('click', () => reorderTextElement('back')); |
|
transparentBgBtn.addEventListener('click', setTransparentBackground); |
|
toggleEffectsBtn.addEventListener('click', toggleEffectsPanel); |
|
fontUpload.addEventListener('change', handleFontUpload); |
|
fontFamily.addEventListener('change', updateFontFamily); |
|
|
|
canvas.addEventListener('mousedown', handleCanvasMouseDown); |
|
canvas.addEventListener('mousemove', handleCanvasMouseMove); |
|
canvas.addEventListener('mouseup', handleCanvasMouseUp); |
|
canvas.addEventListener('mouseleave', handleCanvasMouseUp); |
|
|
|
function saveState() { |
|
|
|
const maxHistorySize = 20; |
|
if (history.length > maxHistorySize) { |
|
history.shift(); |
|
historyIndex--; |
|
} |
|
history = history.slice(0, historyIndex + 1); |
|
history.push(JSON.stringify({ |
|
uploadedImageSrc: uploadedImage ? uploadedImage.src : null, |
|
textElements: textElements |
|
})); |
|
historyIndex = history.length - 1; |
|
updateHistoryButtons(); |
|
} |
|
|
|
function handleImageUpload(e) { |
|
const file = e.target.files[0]; |
|
if (!file) return; |
|
|
|
const originalText = downloadBtn.innerHTML; |
|
downloadBtn.innerHTML = '<i class="fas fa-spinner spinner mr-2"></i> Loading...'; |
|
downloadBtn.disabled = true; |
|
const reader = new FileReader(); |
|
reader.onload = function (event) { |
|
const img = new Image(); |
|
img.onload = function () { |
|
uploadedImage = img; |
|
|
|
try { |
|
fitImageToCanvas(); |
|
removeImageBtn.disabled = false; |
|
fitImageBtn.disabled = false; |
|
saveState(); |
|
aiEnhanceBtn.disabled = false; |
|
downloadBtn.disabled = false; |
|
} catch (error) { |
|
console.error("Error during fitImageToCanvas:", error); |
|
alert('Failed to draw image to canvas. See console for details.'); |
|
} finally { |
|
downloadBtn.innerHTML = originalText; |
|
downloadBtn.disabled = false; |
|
} |
|
}; |
|
img.onerror = function () { |
|
let errorMessage = 'Error loading image.'; |
|
if (img.naturalWidth === 0) { |
|
errorMessage += ' The file might be corrupted or not a valid image.'; |
|
} else { |
|
errorMessage += ' Please try another file.'; |
|
} |
|
showAlert(errorMessage, 'error'); |
|
downloadBtn.innerHTML = originalText; |
|
downloadBtn.disabled = false; |
|
}; |
|
img.src = event.target.result; |
|
}; |
|
reader.onerror = function () { |
|
downloadBtn.innerHTML = originalText; |
|
downloadBtn.disabled = false; |
|
}; |
|
reader.readAsDataURL(file); |
|
} |
|
|
|
function fitImageToCanvas() { |
|
if (!uploadedImage) return; |
|
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height); |
|
|
|
const scale = Math.max( |
|
canvas.width / uploadedImage.width, |
|
canvas.height / uploadedImage.height |
|
); |
|
|
|
const width = uploadedImage.width * scale; |
|
const height = uploadedImage.height * scale; |
|
|
|
const x = (canvas.width - width) / 2; |
|
const y = (canvas.height - height) / 2; |
|
|
|
ctx.drawImage(uploadedImage, x, y, width, height); |
|
|
|
if (gradientCheck.checked) { |
|
applyGradientOverlay(); |
|
} |
|
|
|
redrawTextElements(); |
|
} |
|
|
|
function removeImage() { |
|
uploadedImage = null; |
|
initCanvas(); |
|
saveState(); |
|
redrawTextElements(); |
|
removeImageBtn.disabled = true; |
|
fitImageBtn.disabled = true; |
|
aiEnhanceBtn.disabled = true; |
|
downloadBtn.disabled = textElements.length === 0; |
|
} |
|
|
|
function enhanceImageWithAI() { |
|
if (!uploadedImage) return; |
|
|
|
aiLoadingOverlay.classList.remove('hidden'); |
|
|
|
setTimeout(() => { |
|
|
|
const tempCanvas = document.createElement('canvas'); |
|
tempCanvas.width = uploadedImage.width; |
|
tempCanvas.height = uploadedImage.height; |
|
const tempCtx = tempCanvas.getContext('2d'); |
|
tempCtx.drawImage(uploadedImage, 0, 0); |
|
|
|
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height); |
|
const data = imageData.data; |
|
|
|
for (let i = 0; i < data.length; i += 4) { |
|
|
|
const factor = 1.5; |
|
data[i] = factor * (data[i] - 128) + 128; |
|
data[i + 1] = factor * (data[i + 1] - 128) + 128; |
|
data[i + 2] = factor * (data[i + 2] - 128) + 128; |
|
|
|
data[i] = Math.min(255, data[i] * 1.1); |
|
data[i + 1] = Math.min(255, data[i + 1] * 1.1); |
|
data[i + 2] = Math.min(255, data[i + 2] * 1.1); |
|
|
|
if (i > 0 && i < data.length - 8) { |
|
data[i] = Math.min(255, data[i] * 1.2 - data[i - 4] * 0.2); |
|
data[i + 1] = Math.min(255, data[i + 1] * 1.2 - data[i - 3] * 0.2); |
|
data[i + 2] = Math.min(255, data[i + 2] * 1.2 - data[i - 2] * 0.2); |
|
} |
|
} |
|
tempCtx.putImageData(imageData, 0, 0); |
|
|
|
const enhancedImg = new Image(); |
|
enhancedImg.onload = function () { |
|
uploadedImage = enhancedImg; |
|
fitImageToCanvas(); |
|
saveState(); |
|
aiLoadingOverlay.classList.add('hidden'); |
|
showAlert('Image enhanced successfully!', 'success'); |
|
}; |
|
enhancedImg.src = tempCanvas.toDataURL('image/jpeg', 0.9); |
|
}, 2000); |
|
} |
|
function redrawCanvas() { |
|
if (uploadedImage) { |
|
fitImageToCanvas(); |
|
} else { |
|
initCanvas(); |
|
} |
|
} |
|
|
|
function applyGradientOverlay() { |
|
const opacity = gradientOpacity.value / 100; |
|
const color1 = hexToRgba(gradientColor1.value, opacity); |
|
const color2 = hexToRgba(gradientColor2.value, opacity); |
|
if (gradientType.value === 'multi') { |
|
|
|
ctx.save(); |
|
ctx.globalCompositeOperation = 'multiply'; |
|
|
|
const gradient1 = ctx.createLinearGradient(0, 0, 0, canvas.height); |
|
gradient1.addColorStop(0, color1); |
|
gradient1.addColorStop(0.5, `rgba(${hexToRgb(gradientColor1.value)}, ${opacity * 0.2})`); |
|
gradient1.addColorStop(1, color1); |
|
const gradient2 = ctx.createLinearGradient(0, 0, 0, canvas.height); |
|
gradient2.addColorStop(0, `rgba(${hexToRgb(gradientColor2.value)}, ${opacity * 0.4})`); |
|
gradient2.addColorStop(0.3, `rgba(${hexToRgb(gradientColor2.value)}, ${opacity * 0.1})`); |
|
gradient2.addColorStop(0.7, `rgba(${hexToRgb(gradientColor2.value)}, ${opacity * 0.1})`); |
|
gradient2.addColorStop(1, `rgba(${hexToRgb(gradientColor2.value)}, ${opacity * 0.4})`); |
|
|
|
ctx.fillStyle = gradient1; |
|
ctx.fillRect(0, 0, canvas.width, canvas.height); |
|
|
|
ctx.fillStyle = gradient2; |
|
ctx.fillRect(0, 0, canvas.width, canvas.height); |
|
ctx.restore(); |
|
} else { |
|
let gradient; |
|
switch (gradientType.value) { |
|
case 'top': |
|
gradient = ctx.createLinearGradient(0, 0, 0, canvas.height); |
|
gradient.addColorStop(0, color1); |
|
gradient.addColorStop(1, color2); |
|
break; |
|
case 'bottom': |
|
gradient = ctx.createLinearGradient(0, canvas.height, 0, 0); |
|
gradient.addColorStop(0, color1); |
|
gradient.addColorStop(1, color2); |
|
break; |
|
case 'left': |
|
gradient = ctx.createLinearGradient(0, 0, canvas.width, 0); |
|
gradient.addColorStop(0, color1); |
|
gradient.addColorStop(1, color2); |
|
break; |
|
case 'right': |
|
gradient = ctx.createLinearGradient(canvas.width, 0, 0, 0); |
|
gradient.addColorStop(0, color1); |
|
gradient.addColorStop(1, color2); |
|
break; |
|
case 'vignette': |
|
const centerX = canvas.width / 2; |
|
const centerY = canvas.height / 2; |
|
const radius = Math.max(canvas.width, canvas.height) / 2; |
|
gradient = ctx.createRadialGradient(centerX, centerY, radius * 0.5, centerX, centerY, radius); |
|
gradient.addColorStop(0, color2); |
|
gradient.addColorStop(1, color1); |
|
break; |
|
} |
|
ctx.save(); |
|
ctx.globalCompositeOperation = 'multiply'; |
|
ctx.fillStyle = gradient; |
|
ctx.fillRect(0, 0, canvas.width, canvas.height); |
|
ctx.restore(); |
|
} |
|
} |
|
|
|
function hexToRgba(hex, opacity) { |
|
const r = parseInt(hex.slice(1, 3), 16); |
|
const g = parseInt(hex.slice(3, 5), 16); |
|
const b = parseInt(hex.slice(5, 7), 16); |
|
return `rgba(${r}, ${g}, ${b}, ${opacity})`; |
|
} |
|
|
|
function hexToRgb(hex) { |
|
const r = parseInt(hex.slice(1, 3), 16); |
|
const g = parseInt(hex.slice(3, 5), 16); |
|
const b = parseInt(hex.slice(5, 7), 16); |
|
return `${r}, ${g}, ${b}`; |
|
} |
|
|
|
function redrawTextElements(){ |
|
textElements.forEach(text =>{ |
|
drawTextElement(text); |
|
}); |
|
updateTextElementsList(); |
|
} |
|
function drawTextElement(text){ |
|
ctx.save(); |
|
|
|
|
|
ctx.textAlign = text.align; |
|
|
|
const lines = wrapText(ctx, text.content, text.maxWidth); |
|
const lineHeight = text.size * 1.2; |
|
const textHeight = lines.length * lineHeight; |
|
|
|
let totalWidth = 0; |
|
lines.forEach(line =>{ |
|
const metrics = ctx.measureText(line); |
|
if (metrics.width > totalWidth) { |
|
totalWidth = metrics.width; |
|
} |
|
}); |
|
|
|
|
|
let fontStyle = ''; |
|
fontStyle += text.bold ? 'bold ' : ''; |
|
fontStyle += text.italic ? 'italic ' : ''; |
|
fontStyle += `${text.size}px `; |
|
fontStyle += text.fontFamily || currentFontStyle.fontFamily; |
|
ctx.font = fontStyle; |
|
|
|
|
|
let x = text.x; |
|
if (text.align === 'center') { |
|
x = text.x + totalWidth / 2; |
|
} else if (text.align === 'right') { |
|
x = text.x + totalWidth; |
|
} |
|
|
|
|
|
if (text.bgColor !== 'transparent') { |
|
const bgAlpha = parseInt(text.bgColor.slice(-2), 16) / 255; |
|
ctx.fillStyle = text.bgColor.slice(0, -2) + '00)'; |
|
ctx.globalAlpha = bgAlpha; |
|
const padding = 10; |
|
const bgWidth = totalWidth + padding * 2; |
|
const bgHeight = textHeight + padding * 2; |
|
let bgX = text.x; |
|
if (text.align === 'center') { |
|
bgX = text.x + (totalWidth - bgWidth) / 2; |
|
} else if (text.align === 'right') { |
|
bgX = text.x + totalWidth - bgWidth; |
|
} |
|
ctx.fillRect(bgX, text.y - textHeight - padding / 2, bgWidth, bgHeight); |
|
} |
|
|
|
ctx.fillStyle = text.color; |
|
ctx.globalAlpha = text.opacity / 100; |
|
|
|
|
|
if (text.shadow) { |
|
ctx.shadowColor = text.shadowColor; |
|
ctx.shadowBlur = text.shadowBlur; |
|
ctx.shadowOffsetX = text.shadowOffset.x; |
|
ctx.shadowOffsetY = text.shadowOffset.y; |
|
} else { |
|
ctx.shadowColor = 'transparent'; |
|
ctx.shadowBlur = 0; |
|
ctx.shadowOffsetX = 0; |
|
ctx.shadowOffsetY = 0; |
|
} |
|
|
|
|
|
if (text.outline) { |
|
ctx.strokeStyle = text.outlineColor; |
|
ctx.lineWidth = text.outlineWidth; |
|
|
|
if (text.outlineStyle === 'dashed') { |
|
ctx.setLineDash([5, 3]); |
|
} else if (text.outlineStyle === 'dotted') { |
|
ctx.setLineDash([1, 2]); |
|
} else { |
|
ctx.setLineDash([]); |
|
} |
|
} |
|
|
|
|
|
lines.forEach((line, i) => { |
|
const y = text.y - (lines.length - 1 - i) * lineHeight; |
|
|
|
|
|
let x = text.x; |
|
const metrics = ctx.measureText(line); |
|
|
|
if (text.align === 'center') { |
|
x = text.x - metrics.width / 2; |
|
} else if (text.align === 'right') { |
|
x = text.x - metrics.width; |
|
} |
|
|
|
|
|
if (text.outline) { |
|
ctx.strokeText(line, x, y); |
|
} |
|
|
|
|
|
ctx.fillText(line, x, y); |
|
}); |
|
|
|
|
|
if (text.outline) { |
|
ctx.setLineDash([]); |
|
} |
|
|
|
|
|
if (text.underline) { |
|
ctx.strokeStyle = text.color; |
|
ctx.lineWidth = 2; |
|
ctx.beginPath(); |
|
const lastLineY = text.y; |
|
ctx.moveTo(x - totalWidth / 2, lastLineY + 2); |
|
ctx.lineTo(x + totalWidth / 2, lastLineY + 2); |
|
ctx.stroke(); |
|
} |
|
ctx.restore(); |
|
|
|
if (text === activeTextElement) { |
|
ctx.save(); |
|
ctx.strokeStyle = '#3b82f6'; |
|
ctx.lineWidth = 2; |
|
ctx.setLineDash([5, 5]); |
|
|
|
|
|
const padding = 15; |
|
const boxWidth = text.width + padding * 2; |
|
const boxHeight = text.height + padding * 2; |
|
|
|
let boxX = text.x; |
|
if (text.align === 'center') { |
|
boxX = text.x - boxWidth / 2; |
|
} else if (text.align === 'right') { |
|
boxX = text.x - boxWidth; |
|
} |
|
|
|
const boxY = text.y - text.height - padding; |
|
|
|
|
|
ctx.strokeRect(boxX, boxY, boxWidth, boxHeight); |
|
|
|
|
|
const handleSize = 20; |
|
const resizeHandleX = boxX + boxWidth; |
|
const resizeHandleY = boxY + boxHeight; |
|
|
|
ctx.fillStyle = '#3b82f6'; |
|
ctx.beginPath(); |
|
ctx.arc(resizeHandleX, resizeHandleY, handleSize/2, 0, Math.PI * 2); |
|
ctx.fill(); |
|
|
|
|
|
const rotateHandleX = boxX + boxWidth; |
|
const rotateHandleY = boxY; |
|
|
|
ctx.fillStyle = '#3b82f6'; |
|
ctx.beginPath(); |
|
ctx.arc(rotateHandleX, rotateHandleY, handleSize/2, 0, Math.PI * 2); |
|
ctx.fill(); |
|
|
|
ctx.restore(); |
|
} |
|
} |
|
|
|
function wrapText(context, text, maxWidth) { |
|
if (!text || text.trim() === '') return ['']; |
|
|
|
const words = text.split(/\s+/); |
|
const lines = []; |
|
let currentLine = ''; |
|
|
|
for (let i = 0; i < words.length; i++) { |
|
const word = words[i]; |
|
|
|
const testLine = currentLine ? currentLine + ' ' + word : word; |
|
const metrics = context.measureText(testLine); |
|
|
|
if (metrics.width <= maxWidth || currentLine === '') { |
|
|
|
currentLine = testLine; |
|
} else { |
|
|
|
lines.push(currentLine); |
|
currentLine = word; |
|
} |
|
} |
|
|
|
|
|
if (currentLine !== '') { |
|
lines.push(currentLine); |
|
} |
|
|
|
return lines; |
|
} |
|
function addTextToCanvas() { |
|
const content = myanmarText.value.trim(); |
|
if (!content) { |
|
showAlert('Please enter some text first', 'warning'); |
|
return; |
|
} |
|
const textSize = parseInt(fontSize.value); |
|
const color = fontColor.value; |
|
const bg = bgColor.value === '#00000000' ? 'transparent' : bgColor.value + '00)'; |
|
const opacity = parseInt(textOpacity.value); |
|
const maxWidthValue = parseInt(maxWidth.value); |
|
const fontFamilyValue = fontFamily.value; |
|
|
|
ctx.font = `${currentFontStyle.bold ? 'bold ' : ''}${currentFontStyle.italic ? 'italic ' : ''}${textSize}px ${fontFamilyValue}`; |
|
|
|
const lines = wrapText(ctx, content, maxWidthValue); |
|
const lineHeight = textSize * 1.4; |
|
const textHeight = lines.length * lineHeight; |
|
|
|
let totalWidth = 0; |
|
lines.forEach(line => { |
|
const metrics = ctx.measureText(line); |
|
if (metrics.width > totalWidth) { |
|
totalWidth = metrics.width; |
|
} |
|
}); |
|
const x = (canvas.width - totalWidth) / 2; |
|
const y = (canvas.height + textHeight) / 2; |
|
const textElement = { |
|
id: Date.now(), |
|
content, |
|
x, |
|
y, |
|
width: totalWidth, |
|
height: textHeight, |
|
size: textSize, |
|
color, |
|
bgColor: bg, |
|
opacity, |
|
bold: currentFontStyle.bold, |
|
italic: currentFontStyle.italic, |
|
underline: currentFontStyle.underline, |
|
align: currentFontStyle.align, |
|
outline: outlineCheck.checked, |
|
outlineColor: outlineColor.value, |
|
outlineWidth: parseInt(outlineWidth.value), |
|
outlineStyle: outlineStyle.value, |
|
shadow: shadowCheck.checked, |
|
shadowColor: shadowColor.value, |
|
shadowBlur: parseInt(shadowBlur.value), |
|
shadowOffset: { |
|
x: parseInt(shadowOffsetX.value), |
|
y: parseInt(shadowOffsetY.value) |
|
}, |
|
maxWidth: maxWidthValue, |
|
rotation: 0, |
|
scale: 1, |
|
fontFamily: fontFamilyValue |
|
}; |
|
textElements.push(textElement); |
|
activeTextElement = textElement; |
|
redrawCanvas(); |
|
downloadBtn.disabled = false; |
|
reorderControls.classList.remove('hidden'); |
|
saveState(); |
|
clearAllBtn.disabled = false; |
|
|
|
myanmarText.value = ''; |
|
|
|
showAlert('Text added to thumbnail!', 'success'); |
|
} |
|
|
|
function updateTextElement() { |
|
if (!activeTextElement) return; |
|
const content = myanmarText.value.trim(); |
|
if (!content) { |
|
showAlert('Please enter some text first', 'warning'); |
|
return; |
|
} |
|
|
|
activeTextElement.content = content; |
|
activeTextElement.size = parseInt(fontSize.value); |
|
activeTextElement.color = fontColor.value; |
|
activeTextElement.bgColor = bgColor.value === '#00000000' ? 'transparent' : bgColor.value + '00)'; |
|
activeTextElement.opacity = parseInt(textOpacity.value); |
|
activeTextElement.bold = currentFontStyle.bold; |
|
activeTextElement.italic = currentFontStyle.italic; |
|
activeTextElement.underline = currentFontStyle.underline; |
|
activeTextElement.align = currentFontStyle.align; |
|
activeTextElement.outline = outlineCheck.checked; |
|
activeTextElement.outlineColor = outlineColor.value; |
|
activeTextElement.outlineWidth = parseInt(outlineWidth.value); |
|
activeTextElement.outlineStyle = outlineStyle.value; |
|
activeTextElement.shadow = shadowCheck.checked; |
|
activeTextElement.shadowColor = shadowColor.value; |
|
activeTextElement.shadowBlur = parseInt(shadowBlur.value); |
|
activeTextElement.shadowOffset = { |
|
x: parseInt(shadowOffsetX.value), |
|
y: parseInt(shadowOffsetY.value) |
|
}; |
|
activeTextElement.maxWidth = parseInt(maxWidth.value); |
|
activeTextElement.fontFamily = fontFamily.value; |
|
|
|
ctx.font = `${activeTextElement.bold ? 'bold ' : ''}${activeTextElement.italic ? 'italic ' : ''}${activeTextElement.size}px ${activeTextElement.fontFamily}`; |
|
const lines = wrapText(ctx, activeTextElement.content, activeTextElement.maxWidth); |
|
const lineHeight = activeTextElement.size * 1.4; |
|
activeTextElement.height = lines.length * lineHeight; |
|
|
|
let totalWidth = 0; |
|
lines.forEach(line => { |
|
const metrics = ctx.measureText(line); |
|
if (metrics.width > totalWidth) { |
|
totalWidth = metrics.width; |
|
} |
|
}); |
|
activeTextElement.width = totalWidth; |
|
|
|
redrawCanvas(); |
|
|
|
updateTextBtn.classList.add('hidden'); |
|
addTextBtn.classList.remove('hidden'); |
|
|
|
saveState(); |
|
myanmarText.value = ''; |
|
|
|
showAlert('Text updated successfully!', 'success'); |
|
} |
|
|
|
function handleCanvasMouseDown(e) { |
|
const rect = canvas.getBoundingClientRect(); |
|
const x = e.clientX - rect.left; |
|
const y = e.clientY - rect.top; |
|
|
|
|
|
activeTextElement = null; |
|
for (let i = textElements.length - 1; i >= 0; i--) { |
|
const text = textElements[i]; |
|
const lineHeight = text.size * 1.2; |
|
|
|
|
|
const padding = 15; |
|
const boxWidth = text.width + padding * 2; |
|
const boxHeight = text.height + padding * 2; |
|
|
|
let boxX = text.x; |
|
if (text.align === 'center') { |
|
boxX = text.x - boxWidth / 2; |
|
} else if (text.align === 'right') { |
|
boxX = text.x - boxWidth; |
|
} |
|
|
|
const boxY = text.y - text.height - padding; |
|
|
|
|
|
if (x >= boxX && x <= boxX + boxWidth && |
|
y >= boxY && y <= boxY + boxHeight) { |
|
activeTextElement = text; |
|
|
|
|
|
const handleSize = 20; |
|
const resizeHandleX = boxX + boxWidth - handleSize / 2; |
|
const resizeHandleY = boxY + boxHeight - handleSize / 2; |
|
|
|
if (x >= resizeHandleX - handleSize && x <= resizeHandleX + handleSize && |
|
y >= resizeHandleY - handleSize && y <= resizeHandleY + handleSize) { |
|
isResizing = true; |
|
} |
|
|
|
else if (x >= resizeHandleX - handleSize && x <= resizeHandleX + handleSize && |
|
y >= boxY - handleSize / 2 && y <= boxY + handleSize / 2) { |
|
isRotating = true; |
|
} |
|
else { |
|
isDragging = true; |
|
|
|
if (e.detail === 2) { |
|
editTextElement(text); |
|
} |
|
} |
|
startX = x; |
|
startY = y; |
|
break; |
|
} |
|
} |
|
|
|
if (activeTextElement) { |
|
myanmarText.value = activeTextElement.content; |
|
fontSize.value = activeTextElement.size; |
|
fontSizeValue.textContent = activeTextElement.size; |
|
fontColor.value = activeTextElement.color; |
|
bgColor.value = activeTextElement.bgColor === 'transparent' ? '#00000000' : activeTextElement.bgColor; |
|
textOpacity.value = activeTextElement.opacity; |
|
opacityValue.textContent = activeTextElement.opacity; |
|
|
|
boldBtn.classList.toggle('bg-blue-200', activeTextElement.bold); |
|
italicBtn.classList.toggle('bg-blue-200', activeTextElement.italic); |
|
underlineBtn.classList.toggle('bg-blue-200', activeTextElement.underline); |
|
|
|
setTextAlign(activeTextElement.align); |
|
|
|
outlineCheck.checked = activeTextElement.outline; |
|
outlineColor.value = activeTextElement.outlineColor; |
|
outlineWidth.value = activeTextElement.outlineWidth; |
|
outlineStyle.value = activeTextElement.outlineStyle; |
|
shadowCheck.checked = activeTextElement.shadow; |
|
shadowColor.value = activeTextElement.shadowColor; |
|
shadowBlur.value = activeTextElement.shadowBlur; |
|
shadowOffsetX.value = activeTextElement.shadowOffset.x; |
|
shadowOffsetY.value = activeTextElement.shadowOffset.y; |
|
maxWidth.value = activeTextElement.maxWidth; |
|
maxWidthValue.textContent = activeTextElement.maxWidth; |
|
toggleOutline(); |
|
toggleShadow(); |
|
} |
|
redrawCanvas(); |
|
updateReorderButtons(); |
|
} |
|
|
|
function editTextElement(text) { |
|
|
|
if (textEditInput) { |
|
textEditInput.remove(); |
|
textEditInput = null; |
|
} |
|
|
|
textEditInput = document.createElement('textarea'); |
|
textEditInput.className = 'text-edit-input'; |
|
textEditInput.style.fontFamily = text.fontFamily || currentFontStyle.fontFamily; |
|
textEditInput.value = text.content; |
|
textEditInput.style.fontSize = `${text.size}px`; |
|
textEditInput.style.color = text.color; |
|
textEditInput.style.fontWeight = text.bold ? 'bold' : 'normal'; |
|
textEditInput.style.fontStyle = text.italic ? 'italic' : 'normal'; |
|
textEditInput.style.textDecoration = text.underline ? 'underline' : 'none'; |
|
textEditInput.rows = 3; |
|
|
|
const lineHeight = text.size * 1.4; |
|
const lines = wrapText(ctx, text.content, text.maxWidth); |
|
const textHeight = lines.length * lineHeight; |
|
const padding = 5; |
|
const boxWidth = text.width + padding * 2; |
|
const boxHeight = textHeight + padding * 2; |
|
let boxX = text.x; |
|
if (text.align === 'center') { |
|
boxX = text.x - boxWidth / 2; |
|
} else if (text.align === 'right') { |
|
boxX = text.x - boxWidth; |
|
} |
|
|
|
const canvasRect = canvas.getBoundingClientRect(); |
|
textEditInput.style.position = 'absolute'; |
|
textEditInput.style.left = `${canvasRect.left + boxX}px`; |
|
textEditInput.style.top = `${canvasRect.top + text.y - textHeight - padding}px`; |
|
textEditInput.style.width = `${boxWidth}px`; |
|
textEditInput.style.height = `${boxHeight}px`; |
|
|
|
document.body.appendChild(textEditInput); |
|
textEditInput.focus(); |
|
|
|
textEditInput.addEventListener('blur', function () { |
|
if (textEditInput) { |
|
text.content = textEditInput.value; |
|
textEditInput.remove(); |
|
textEditInput = null; |
|
redrawCanvas(); |
|
} |
|
saveState(); |
|
}); |
|
textEditInput.addEventListener('keydown', function (e) { |
|
if (e.key === 'Enter' && !e.shiftKey) { |
|
e.preventDefault(); |
|
text.content = textEditInput.value; |
|
textEditInput.remove(); |
|
textEditInput = null; |
|
redrawCanvas(); |
|
} else if (e.key === 'Escape') { |
|
textEditInput.remove(); |
|
textEditInput = null; |
|
redrawCanvas(); |
|
} |
|
saveState(); |
|
}); |
|
} |
|
function handleCanvasMouseMove(e) { |
|
if (!isDragging && !isResizing && !isRotating && !isScaling) return; |
|
const rect = canvas.getBoundingClientRect(); |
|
const x = e.clientX - rect.left; |
|
const y = e.clientY - rect.top; |
|
if (isDragging && activeTextElement) { |
|
isManipulating = true; |
|
|
|
const dx = x - startX; |
|
const dy = y - startY; |
|
activeTextElement.x += dx; |
|
activeTextElement.y += dy; |
|
startX = x; |
|
startY = y; |
|
} |
|
else if (isResizing && activeTextElement) { |
|
isManipulating = true; |
|
|
|
const dy = startY - y; |
|
const scale = 1 + dy / 50; |
|
|
|
let newSize = Math.max(10, Math.min(100, activeTextElement.size * scale)); |
|
if (newSize !== activeTextElement.size) { |
|
activeTextElement.size = newSize; |
|
const lines = wrapText(ctx, activeTextElement.content, activeTextElement.maxWidth); |
|
const lineHeight = activeTextElement.size * 1.2; |
|
activeTextElement.height = lines.length * lineHeight; |
|
|
|
let totalWidth = 0; |
|
lines.forEach(line => { |
|
const metrics = ctx.measureText(line); |
|
if (metrics.width > totalWidth) { |
|
totalWidth = metrics.width; |
|
} |
|
}); |
|
activeTextElement.width = totalWidth; |
|
startY = y; |
|
} |
|
} |
|
else if (isRotating && activeTextElement) { |
|
isManipulating = true; |
|
|
|
const centerX = activeTextElement.x + activeTextElement.width / 2; |
|
const centerY = activeTextElement.y - activeTextElement.height / 2; |
|
const angle = Math.atan2(y - centerY, x - centerX) - Math.atan2(startY - centerY, startX - centerY); |
|
activeTextElement.rotation += angle * (180 / Math.PI); |
|
startX = x; |
|
startY = y; |
|
redrawCanvas(); |
|
} |
|
else if (isScaling && activeTextElement) { |
|
isManipulating = true; |
|
|
|
const dx = x - startX; |
|
const scale = 1 + dx / 100; |
|
|
|
let newMaxWidth = Math.max(50, Math.min(1800, activeTextElement.maxWidth * scale)); |
|
if (newMaxWidth !== activeTextElement.maxWidth) { |
|
activeTextElement.maxWidth = newMaxWidth; |
|
|
|
ctx.font = `${activeTextElement.bold ? 'bold ' : ''}${activeTextElement.italic ? 'italic ' : ''}${activeTextElement.size}px ${activeTextElement.fontFamily}`; |
|
const lines = wrapText(ctx, activeTextElement.content, activeTextElement.maxWidth); |
|
const lineHeight = activeTextElement.size * 1.2; |
|
activeTextElement.height = lines.length * lineHeight; |
|
|
|
let totalWidth = 0; |
|
lines.forEach(line => { |
|
const metrics = ctx.measureText(line); |
|
if (metrics.width > totalWidth) { |
|
totalWidth = metrics.width; |
|
} |
|
}); |
|
activeTextElement.width = totalWidth; |
|
startX = x; |
|
redrawCanvas(); |
|
} |
|
} |
|
redrawCanvas(); |
|
} |
|
function handleCanvasMouseUp() { |
|
isManipulating = false; |
|
isDragging = false; |
|
isResizing = false; |
|
isRotating = false; |
|
isScaling = false; |
|
saveState(); |
|
} |
|
canvas.addEventListener('mouseup', redrawCanvas); |
|
|
|
function downloadThumbnail() { |
|
|
|
const originalText = downloadBtn.innerHTML; |
|
downloadBtn.innerHTML = '<i class="fas fa-spinner spinner mr-2"></i> Processing...'; |
|
downloadBtn.disabled = true; |
|
|
|
setTimeout(() => { |
|
const link = document.createElement('a'); |
|
link.download = 'thumbnail-' + Date.now() + '.png'; |
|
link.href = canvas.toDataURL('image/png'); |
|
document.body.appendChild(link); |
|
link.click(); |
|
document.body.removeChild(link); |
|
|
|
downloadBtn.innerHTML = originalText; |
|
downloadBtn.disabled = false; |
|
|
|
showAlert('Thumbnail downloaded successfully!', 'success'); |
|
}, 100); |
|
} |
|
|
|
function clearAllElements() { |
|
if (confirm('Are you sure you want to remove all text elements?')) { |
|
textElements = []; |
|
activeTextElement = null; |
|
saveState(); |
|
redrawCanvas(); |
|
reorderControls.classList.add('hidden'); |
|
downloadBtn.disabled = !uploadedImage; |
|
clearAllBtn.disabled = true; |
|
|
|
showAlert('All text elements removed', 'info'); |
|
} |
|
} |
|
|
|
function updateFontSizeValue() { |
|
fontSizeValue.textContent = fontSize.value; |
|
} |
|
|
|
function updateOpacityValue() { |
|
opacityValue.textContent = textOpacity.value; |
|
} |
|
|
|
function toggleBold() { |
|
currentFontStyle.bold = !currentFontStyle.bold; |
|
boldBtn.classList.toggle('bg-blue-200', currentFontStyle.bold); |
|
|
|
if (activeTextElement) { |
|
activeTextElement.bold = currentFontStyle.bold; |
|
} |
|
} |
|
|
|
function toggleItalic() { |
|
currentFontStyle.italic = !currentFontStyle.italic; |
|
italicBtn.classList.toggle('bg-blue-200', currentFontStyle.italic); |
|
|
|
if (activeTextElement) { |
|
activeTextElement.italic = currentFontStyle.italic; |
|
} |
|
} |
|
|
|
function toggleUnderline() { |
|
currentFontStyle.underline = !currentFontStyle.underline; |
|
underlineBtn.classList.toggle('bg-blue-200', currentFontStyle.underline); |
|
|
|
if (activeTextElement) { |
|
activeTextElement.underline = currentFontStyle.underline; |
|
} |
|
} |
|
|
|
function setTextAlign(align) { |
|
currentFontStyle.align = align; |
|
|
|
alignLeftBtn.classList.remove('bg-blue-200'); |
|
alignCenterBtn.classList.remove('bg-blue-200'); |
|
alignRightBtn.classList.remove('bg-blue-200'); |
|
if (align === 'left') { |
|
alignLeftBtn.classList.add('bg-blue-200'); |
|
} else if (align === 'center') { |
|
alignCenterBtn.classList.add('bg-blue-200'); |
|
} else if (align === 'right') { |
|
alignRightBtn.classList.add('bg-blue-200'); |
|
} |
|
|
|
if (activeTextElement) { |
|
activeTextElement.align = align; |
|
} |
|
} |
|
|
|
function toggleOutline() { |
|
const enabled = outlineCheck.checked; |
|
outlineColor.disabled = !enabled; |
|
outlineWidth.disabled = !enabled; |
|
currentFontStyle.outline = enabled; |
|
outlineStyle.disabled = !enabled; |
|
|
|
|
|
if (activeTextElement) { |
|
activeTextElement.outline = enabled; |
|
activeTextElement.outlineColor = outlineColor.value; |
|
activeTextElement.outlineWidth = parseInt(outlineWidth.value); |
|
activeTextElement.outlineStyle = outlineStyle.value; |
|
} |
|
} |
|
|
|
function updateOutlineColor() { |
|
currentFontStyle.outlineColor = outlineColor.value; |
|
if (activeTextElement) { |
|
activeTextElement.outlineColor = outlineColor.value; |
|
} |
|
} |
|
|
|
function updateOutlineWidth() { |
|
currentFontStyle.outlineWidth = parseInt(outlineWidth.value); |
|
if (activeTextElement) { |
|
activeTextElement.outlineWidth = parseInt(outlineWidth.value); |
|
} |
|
} |
|
|
|
function toggleShadow() { |
|
const enabled = shadowCheck.checked; |
|
shadowColor.disabled = !enabled; |
|
shadowBlur.disabled = !enabled; |
|
shadowOffsetX.disabled = !enabled; |
|
shadowOffsetY.disabled = !enabled; |
|
currentFontStyle.shadow = enabled; |
|
|
|
|
|
if (activeTextElement) { |
|
activeTextElement.shadow = enabled; |
|
activeTextElement.shadowColor = shadowColor.value; |
|
activeTextElement.shadowBlur = parseInt(shadowBlur.value); |
|
activeTextElement.shadowOffset = { |
|
x: parseInt(shadowOffsetX.value), |
|
y: parseInt(shadowOffsetY.value) |
|
}; |
|
} |
|
} |
|
|
|
function updateOutlineStyle() { |
|
currentFontStyle.outlineStyle = outlineStyle.value; |
|
if (activeTextElement) { |
|
activeTextElement.outlineStyle = outlineStyle.value; |
|
} |
|
} |
|
|
|
function updateShadowBlur() { |
|
console.log('Updating shadow blur', shadowBlur.value); |
|
currentFontStyle.shadowBlur = parseInt(shadowBlur.value); |
|
if (activeTextElement) { |
|
activeTextElement.shadowBlur = parseInt(shadowBlur.value); |
|
} |
|
} |
|
|
|
function updateShadowOffset(e) { |
|
console.log('Updating shadow offset'); |
|
currentFontStyle.shadowOffset.x = parseInt(shadowOffsetX.value); |
|
currentFontStyle.shadowOffset.y = parseInt(shadowOffsetY.value); |
|
console.log('New offset:', currentFontStyle.shadowOffset); |
|
if (isNaN(currentFontStyle.shadowOffset.x)) currentFontStyle.shadowOffset.x = 0; |
|
if (isNaN(currentFontStyle.shadowOffset.y)) currentFontStyle.shadowOffset.y = 0; |
|
if (activeTextElement) { |
|
activeTextElement.shadowOffset = { |
|
x: parseInt(shadowOffsetX.value), |
|
y: parseInt(shadowOffsetY.value) |
|
}; |
|
updateTextElement(); |
|
} |
|
} |
|
|
|
function updateShadowColor() { |
|
currentFontStyle.shadowColor = shadowColor.value; |
|
if (activeTextElement) { |
|
activeTextElement.shadowColor = shadowColor.value; |
|
} |
|
} |
|
|
|
function updateMaxWidth() { |
|
maxWidthValue.textContent = maxWidth.value; |
|
currentFontStyle.maxWidth = parseInt(maxWidth.value); |
|
} |
|
|
|
function toggleGradient() { |
|
const enabled = gradientCheck.checked; |
|
gradientType.disabled = !enabled; |
|
gradientColor1.disabled = !enabled; |
|
gradientColor2.disabled = !enabled; |
|
gradientOpacity.disabled = !enabled; |
|
redrawCanvas(); |
|
} |
|
|
|
function updateGradientOpacity() { |
|
gradientOpacityValue.textContent = gradientOpacity.value; |
|
redrawCanvas(); |
|
} |
|
|
|
function generateRandomColor() { |
|
const randomColor = '#' + Math.floor(Math.random() * 16777215).toString(16); |
|
fontColor.value = randomColor; |
|
} |
|
|
|
function setTransparentBackground() { |
|
bgColor.value = '#00000000'; |
|
} |
|
|
|
function toggleEffectsPanel() { |
|
effectsPanel.classList.toggle('hidden'); |
|
const icon = toggleEffectsBtn.querySelector('i'); |
|
if (effectsPanel.classList.contains('hidden')) { |
|
toggleEffectsBtn.innerHTML = '<i class="fas fa-chevron-down mr-1"></i> Show'; |
|
} else { |
|
toggleEffectsBtn.innerHTML = '<i class="fas fa-chevron-up mr-1"></i> Hide'; |
|
} |
|
} |
|
|
|
function handleFontUpload(e) { |
|
const file = e.target.files[0]; |
|
if (!file) return; |
|
|
|
fontLoadingIndicator.classList.remove('hidden'); |
|
fontFamily.disabled = true; |
|
addTextBtn.disabled = true; |
|
updateTextBtn.disabled = true; |
|
loadedFontNameContainer.classList.add('hidden'); |
|
saveState(); |
|
|
|
const reader = new FileReader(); |
|
reader.onload = function (event) { |
|
const fontName = file.name.replace(/\.[^/.]+$/, ""); |
|
const fontFace = new FontFace(fontName, `url(${event.target.result})`); |
|
fontFace.load().then(function (loadedFace) { |
|
document.fonts.add(loadedFace); |
|
customFonts[fontName] = loadedFace; |
|
|
|
const option = document.createElement('option'); |
|
option.value = fontName; |
|
option.textContent = fontName; |
|
fontFamily.appendChild(option); |
|
|
|
if (!activeTextElement) { |
|
fontFamily.value = fontName; |
|
currentFontStyle.fontFamily = fontName; |
|
} |
|
|
|
document.getElementById('loadedFontName').textContent = fontName; |
|
loadedFontNameContainer.classList.remove('hidden'); |
|
showAlert('Font loaded successfully!', 'success'); |
|
fontLoadingIndicator.classList.add('hidden'); |
|
fontFamily.disabled = false; |
|
addTextBtn.disabled = false; |
|
updateTextBtn.disabled = false; |
|
|
|
}).catch(function (error) { |
|
fontLoadingIndicator.classList.add('hidden'); |
|
fontFamily.disabled = false; |
|
addTextBtn.disabled = false; |
|
updateTextBtn.disabled = false; |
|
|
|
let errorMessage = 'Error loading font.'; |
|
if (error.message.includes('network')) { |
|
errorMessage += ' Please check the font file URL or your network connection.'; |
|
} |
|
showAlert('Error loading font: ' + error.message, 'error'); |
|
}); |
|
}; |
|
reader.readAsDataURL(file); |
|
|
|
} |
|
|
|
function updateFontFamily() { |
|
currentFontStyle.fontFamily = fontFamily.value; |
|
|
|
if (activeTextElement) { |
|
updateTextElement(); |
|
} else { |
|
} |
|
} |
|
|
|
function updateTextElementsList() { |
|
textElementsList.innerHTML = ''; |
|
if (textElements.length === 0) { |
|
textElementsList.innerHTML = ` |
|
<div class="text-center py-4 text-gray-500"> |
|
<i class="fas fa-info-circle mr-1"></i> No text elements added yet |
|
</div> |
|
`; |
|
return; |
|
} |
|
textElements.forEach((text, index) => { |
|
const element = document.createElement('div'); |
|
element.className = 'flex items-center justify-between p-3 bg-gray-50 rounded-lg border border-gray-200 mb-2'; |
|
const preview = document.createElement('div'); |
|
preview.className = 'truncate flex-1'; |
|
preview.style.fontFamily = text.fontFamily || currentFontStyle.fontFamily; |
|
preview.textContent = text.content; |
|
preview.style.fontSize = `${Math.min(16, text.size)}px`; |
|
preview.style.color = text.color; |
|
if (text.bold) preview.style.fontWeight = 'bold'; |
|
if (text.italic) preview.style.fontStyle = 'italic'; |
|
if (text.underline) preview.style.textDecoration = 'underline'; |
|
const buttons = document.createElement('div'); |
|
buttons.className = 'flex space-x-2'; |
|
const editBtn = document.createElement('button'); |
|
editBtn.className = 'px-2 py-1 bg-blue-100 text-blue-600 rounded hover:bg-blue-200 tooltip'; |
|
editBtn.innerHTML = '<i class="fas fa-edit"></i>'; |
|
editBtn.title = 'Edit'; |
|
editBtn.addEventListener('click', () => { |
|
activeTextElement = text; |
|
redrawCanvas(); |
|
|
|
myanmarText.value = text.content; |
|
fontSize.value = text.size; |
|
fontSizeValue.textContent = text.size; |
|
fontColor.value = text.color; |
|
bgColor.value = text.bgColor === 'transparent' ? '#00000000' : text.bgColor; |
|
textOpacity.value = text.opacity; |
|
opacityValue.textContent = text.opacity; |
|
|
|
currentFontStyle.bold = text.bold; |
|
currentFontStyle.italic = text.italic; |
|
currentFontStyle.underline = text.underline; |
|
currentFontStyle.align = text.align; |
|
boldBtn.classList.toggle('bg-blue-200', text.bold); |
|
italicBtn.classList.toggle('bg-blue-200', text.italic); |
|
underlineBtn.classList.toggle('bg-blue-200', text.underline); |
|
setTextAlign(text.align); |
|
|
|
outlineCheck.checked = text.outline; |
|
outlineColor.value = text.outlineColor; |
|
outlineWidth.value = text.outlineWidth; |
|
outlineStyle.value = text.outlineStyle; |
|
shadowCheck.checked = text.shadow; |
|
shadowColor.value = text.shadowColor; |
|
shadowBlur.value = text.shadowBlur; |
|
shadowOffsetX.value = text.shadowOffset.x; |
|
shadowOffsetY.value = text.shadowOffset.y; |
|
maxWidth.value = text.maxWidth; |
|
maxWidthValue.textContent = text.maxWidth; |
|
toggleOutline(); |
|
toggleShadow(); |
|
|
|
addTextBtn.classList.add('hidden'); |
|
updateTextBtn.classList.remove('hidden'); |
|
}); |
|
const deleteBtn = document.createElement('button'); |
|
deleteBtn.className = 'px-2 py-1 bg-red-100 text-red-600 rounded hover:bg-red-200 tooltip'; |
|
deleteBtn.innerHTML = '<i class="fas fa-trash-alt"></i>'; |
|
deleteBtn.title = 'Delete'; |
|
deleteBtn.addEventListener('click', () => { |
|
if (confirm('Are you sure you want to delete this text element?')) { |
|
textElements = textElements.filter(t => t.id !== text.id); |
|
if (activeTextElement === text) { |
|
activeTextElement = null; |
|
} |
|
updateReorderButtons(); |
|
saveState(); |
|
redrawCanvas(); |
|
showAlert('Text element deleted successfully!', 'info'); |
|
} |
|
}); |
|
buttons.appendChild(editBtn); |
|
buttons.appendChild(deleteBtn); |
|
element.appendChild(preview); |
|
element.appendChild(buttons); |
|
textElementsList.appendChild(element); |
|
}); |
|
} |
|
|
|
function handleThumbnailSizeChange() { |
|
const selectedSize = thumbnailSize.value; |
|
if (selectedSize === 'custom') { |
|
customSizeContainer.classList.remove('hidden'); |
|
} else { |
|
customSizeContainer.classList.add('hidden'); |
|
|
|
const [width, height] = selectedSize.split('x').map(Number); |
|
canvas.width = width; |
|
canvas.height = height; |
|
redrawCanvas(); |
|
} |
|
} |
|
|
|
function applyCanvasSize() { |
|
const selectedSize = thumbnailSize.value; |
|
if (selectedSize === 'custom') { |
|
const width = parseInt(customWidth.value); |
|
const height = parseInt(customHeight.value); |
|
if (isNaN(width) || isNaN(height) || width <= 0 || height <= 0) { |
|
showAlert('Please enter valid custom dimensions.', 'warning'); |
|
return; |
|
} |
|
canvas.width = width; |
|
canvas.height = height; |
|
saveState(); |
|
redrawCanvas(); |
|
} else { |
|
handleThumbnailSizeChange(); |
|
} |
|
} |
|
|
|
|
|
function showAlert(message, type = 'info') { |
|
const alertContainer = document.createElement('div'); |
|
alertContainer.className = `fixed top-4 right-4 z-50 bg-${type === 'success' ? 'green' : type === 'warning' ? 'yellow' : 'blue'}-100 border border-${type === 'success' ? 'green' : type === 'warning' ? 'yellow' : 'blue'}-500 text-${type === 'success' ? 'green' : type === 'warning' ? 'yellow' : 'blue'}-700 px-4 py-3 rounded relative`; |
|
alertContainer.innerHTML = ` |
|
<strong class="font-bold">${type.toUpperCase()}:</strong> |
|
<span class="block sm:inline">${message}</span> |
|
`; |
|
document.body.appendChild(alertContainer); |
|
|
|
setTimeout(() => { |
|
document.body.removeChild(alertContainer); |
|
}, 3000); |
|
} |
|
|
|
function undo() { |
|
if (historyIndex > 0) { |
|
historyIndex--; |
|
loadState(history[historyIndex]); |
|
updateHistoryButtons(); |
|
} |
|
} |
|
|
|
function redo() { |
|
if (historyIndex < history.length - 1) { |
|
historyIndex++; |
|
loadState(history[historyIndex]); |
|
updateHistoryButtons(); |
|
} |
|
} |
|
|
|
function loadState(stateJson) { |
|
const state = JSON.parse(stateJson); |
|
textElements = state.textElements; |
|
activeTextElement = null; |
|
|
|
if (state.uploadedImageSrc) { |
|
uploadedImage = new Image(); |
|
uploadedImage.onload = redrawCanvas; |
|
uploadedImage.src = state.uploadedImageSrc; |
|
} else { |
|
uploadedImage = null; |
|
redrawCanvas(); |
|
} |
|
updateTextElementsList(); |
|
} |
|
|
|
function reorderTextElement(action) { |
|
if (!activeTextElement) return; |
|
const currentIndex = textElements.indexOf(activeTextElement); |
|
if (currentIndex === -1) return; |
|
let newIndex = currentIndex; |
|
switch (action) { |
|
case 'forward': |
|
newIndex = Math.min(textElements.length - 1, currentIndex + 1); |
|
break; |
|
case 'backward': |
|
newIndex = Math.max(0, currentIndex - 1); |
|
break; |
|
case 'front': |
|
newIndex = textElements.length - 1; |
|
break; |
|
case 'back': |
|
newIndex = 0; |
|
break; |
|
} |
|
if (newIndex !== currentIndex) { |
|
|
|
const [elementToMove] = textElements.splice(currentIndex, 1); |
|
|
|
textElements.splice(newIndex, 0, elementToMove); |
|
saveState(); |
|
redrawCanvas(); |
|
updateReorderButtons(); |
|
} |
|
} |
|
function updateReorderButtons() { |
|
if (!activeTextElement) { |
|
bringForwardBtn.disabled = true; |
|
sendBackwardBtn.disabled = true; |
|
bringToFrontBtn.disabled = true; |
|
sendToBackBtn.disabled = true; |
|
return; |
|
} |
|
const currentIndex = textElements.indexOf(activeTextElement); |
|
if (currentIndex === -1) { |
|
|
|
bringForwardBtn.disabled = true; |
|
sendBackwardBtn.disabled = true; |
|
bringToFrontBtn.disabled = true; |
|
sendToBackBtn.disabled = true; |
|
return; |
|
} |
|
|
|
bringForwardBtn.disabled = currentIndex === textElements.length - 1; |
|
sendBackwardBtn.disabled = currentIndex === 0; |
|
bringToFrontBtn.disabled = currentIndex === textElements.length - 1; |
|
sendToBackBtn.disabled = currentIndex === 0; |
|
|
|
reorderControls.classList.remove('hidden'); |
|
} |
|
|
|
function updateHistoryButtons() { |
|
undoBtn.disabled = historyIndex <= 0; |
|
redoBtn.disabled = historyIndex >= history.length - 1; |
|
} |
|
|
|
saveState(); |
|
|
|
redrawCanvas(); |
|
</script> |
|
</body> |
|
|
|
</html> |