📄 plaintext
public
Untitled Paste
Guest
1h ago
9 views
Text Paste
| 1 | <!DOCTYPE html> |
| 2 | <html lang="th"> |
| 3 | <head> |
| 4 | <meta charset="UTF-8"> |
| 5 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| 6 | <title>AI Image Detector & Editor (Canva Style)</title> |
| 7 | <script src="https://cdn.tailwindcss.com"></script> |
| 8 | <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> |
| 9 | <!-- นำเข้า Google Fonts ภาษาไทย --> |
| 10 | <link href="https://fonts.googleapis.com/css2?family=Kanit:wght@300;400;500;700&family=Prompt:wght@300;400;500;700&display=swap" rel="stylesheet"> |
| 11 | <!-- นำเข้า Fabric.js สำหรับระบบ Layer และการจัดการ Object แบบ Canva --> |
| 12 | <script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.1/fabric.min.js"></script> |
| 13 | <style> |
| 14 | ::-webkit-scrollbar { width: 8px; } |
| 15 | ::-webkit-scrollbar-track { background: #f1f1f1; } |
| 16 | ::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; } |
| 17 | ::-webkit-scrollbar-thumb:hover { background: #94a3b8; } |
| 18 | |
| 19 | /* จัดการ Container ของ Canvas ที่ถูกสร้างโดย Fabric.js */ |
| 20 | .canvas-container { |
| 21 | margin: 0 auto; |
| 22 | box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); |
| 23 | } |
| 24 | </style> |
| 25 | </head> |
| 26 | <body class="bg-slate-50 text-slate-800 font-sans min-h-screen"> |
| 27 | |
| 28 | <div class="max-w-7xl mx-auto px-4 py-8"> |
| 29 | |
| 30 | <!-- Header --> |
| 31 | <header class="text-center mb-10"> |
| 32 | <h1 class="text-3xl md:text-4xl font-bold text-indigo-700 mb-2"> |
| 33 | <i class="fa-solid fa-layer-group mr-2"></i> AI Image Detector & Editor |
| 34 | </h1> |
| 35 | <p class="text-slate-500">ตรวจสอบภาพ AI และระบบแก้ไขภาพแบบเลเยอร์ (Canva Style)</p> |
| 36 | </header> |
| 37 | |
| 38 | <!-- Main Content Grid --> |
| 39 | <div class="grid grid-cols-1 lg:grid-cols-12 gap-6"> |
| 40 | |
| 41 | <!-- Left Column: Controls --> |
| 42 | <div class="lg:col-span-4 xl:col-span-3 space-y-6"> |
| 43 | |
| 44 | <!-- Upload Section --> |
| 45 | <div class="bg-white p-5 rounded-2xl shadow-sm border border-slate-200"> |
| 46 | <h2 class="text-lg font-semibold mb-4 text-slate-700">1. ภาพหลัก (Background)</h2> |
| 47 | <label for="imageUpload" class="flex flex-col items-center justify-center w-full h-28 border-2 border-slate-300 border-dashed rounded-xl cursor-pointer bg-slate-50 hover:bg-slate-100 transition"> |
| 48 | <div class="flex flex-col items-center justify-center pt-5 pb-6"> |
| 49 | <i class="fa-solid fa-cloud-arrow-up text-2xl text-indigo-500 mb-2"></i> |
| 50 | <p class="text-sm text-slate-500"><span class="font-semibold">อัปโหลดภาพหลัก</span></p> |
| 51 | </div> |
| 52 | <input id="imageUpload" type="file" class="hidden" accept="image/*" /> |
| 53 | </label> |
| 54 | </div> |
| 55 | |
| 56 | <!-- Detection Status --> |
| 57 | <div id="detectionPanel" class="bg-white p-5 rounded-2xl shadow-sm border border-slate-200 hidden"> |
| 58 | <h2 class="text-lg font-semibold mb-4 text-slate-700">2. ผลการวิเคราะห์ภาพ</h2> |
| 59 | |
| 60 | <div id="detectionLoading" class="flex flex-col items-center py-4"> |
| 61 | <i class="fa-solid fa-circle-notch fa-spin text-4xl text-indigo-500 mb-3"></i> |
| 62 | <p class="text-sm text-slate-600 font-medium" id="loadingText">กำลังตรวจสอบ Metadata...</p> |
| 63 | <div class="w-full bg-slate-200 rounded-full h-2.5 mt-4"> |
| 64 | <div id="loadingBar" class="bg-indigo-600 h-2.5 rounded-full" style="width: 0%"></div> |
| 65 | </div> |
| 66 | </div> |
| 67 | |
| 68 | <div id="detectionResult" class="hidden text-center"> |
| 69 | <div id="resultIcon" class="text-5xl mb-3"></div> |
| 70 | <h3 id="resultTitle" class="text-lg font-bold mb-1"></h3> |
| 71 | <p id="resultDesc" class="text-xs text-slate-500 mb-4"></p> |
| 72 | |
| 73 | <div class="bg-slate-50 rounded-lg p-3 text-left text-xs text-slate-600 space-y-2 mb-4"> |
| 74 | <div class="flex justify-between"><span>โอกาสเป็นภาพ AI:</span> <span id="scoreAi" class="font-semibold"></span></div> |
| 75 | <div class="flex justify-between"><span>Noise Pattern:</span> <span id="scoreNoise" class="font-semibold"></span></div> |
| 76 | </div> |
| 77 | |
| 78 | <button id="startEditBtn" class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-medium py-2 px-4 rounded-lg transition"> |
| 79 | <i class="fa-solid fa-wand-magic-sparkles mr-2"></i>เปิดเครื่องมือแก้ไข (Layers) |
| 80 | </button> |
| 81 | </div> |
| 82 | </div> |
| 83 | |
| 84 | <!-- Editor Tools (Layers, Filters, Draw) --> |
| 85 | <div id="editorPanel" class="bg-white p-5 rounded-2xl shadow-sm border border-slate-200 hidden max-h-[800px] overflow-y-auto"> |
| 86 | <h2 class="text-lg font-semibold mb-4 text-slate-700">3. เครื่องมือแก้ไข (Canva Style)</h2> |
| 87 | |
| 88 | <!-- Add Elements / Layers --> |
| 89 | <div class="mb-5"> |
| 90 | <label class="block text-sm font-medium text-slate-700 mb-2"><i class="fa-solid fa-shapes mr-1"></i> เพิ่มองค์ประกอบ</label> |
| 91 | <div class="grid grid-cols-2 gap-2"> |
| 92 | <button id="addTextBtn" class="bg-slate-100 hover:bg-slate-200 text-slate-700 py-2 rounded text-sm transition border border-slate-200"> |
| 93 | <i class="fa-solid fa-font"></i> ข้อความ |
| 94 | </button> |
| 95 | <label class="bg-slate-100 hover:bg-slate-200 text-slate-700 py-2 rounded text-sm transition border border-slate-200 text-center cursor-pointer"> |
| 96 | <i class="fa-regular fa-image"></i> เพิ่มรูปภาพ |
| 97 | <input type="file" id="addLayerImage" class="hidden" accept="image/*"> |
| 98 | </label> |
| 99 | <button id="addRectBtn" class="bg-slate-100 hover:bg-slate-200 text-slate-700 py-2 rounded text-sm transition border border-slate-200"> |
| 100 | <i class="fa-solid fa-square"></i> สี่เหลี่ยม |
| 101 | </button> |
| 102 | <button id="addCircleBtn" class="bg-slate-100 hover:bg-slate-200 text-slate-700 py-2 rounded text-sm transition border border-slate-200"> |
| 103 | <i class="fa-solid fa-circle"></i> วงกลม |
| 104 | </button> |
| 105 | </div> |
| 106 | |
| 107 | <!-- เครื่องมือลบและแก้ไขข้อความบนภาพ --> |
| 108 | <div class="mt-3"> |
| 109 | <button id="magicTextBtn" class="w-full bg-gradient-to-r from-purple-500 to-indigo-500 hover:from-purple-600 hover:to-indigo-600 text-white py-2 rounded-lg text-sm font-medium transition shadow-sm"> |
| 110 | <i class="fa-solid fa-wand-magic-sparkles mr-1"></i> แก้ไขข้อความเดิมบนภาพ (Magic Edit) |
| 111 | </button> |
| 112 | <p class="text-[10px] text-slate-500 mt-1 text-center">คลิกปุ่ม แล้วลากคลุมข้อความเดิม เพื่อลบและพิมพ์ใหม่</p> |
| 113 | </div> |
| 114 | </div> |
| 115 | |
| 116 | <!-- Manage Selected Object --> |
| 117 | <div id="objectControls" class="mb-5 p-3 bg-indigo-50 border border-indigo-100 rounded-lg opacity-50 pointer-events-none transition-opacity"> |
| 118 | <div class="flex justify-between items-center mb-2"> |
| 119 | <label class="block text-sm font-medium text-indigo-900">จัดการวัตถุที่เลือก</label> |
| 120 | <input type="color" id="objColorPicker" class="w-6 h-6 rounded cursor-pointer border-0 p-0 hidden" title="เปลี่ยนสีวัตถุ/ข้อความ"> |
| 121 | </div> |
| 122 | <div class="flex space-x-2"> |
| 123 | <button id="bringForwardBtn" class="flex-1 bg-white hover:bg-indigo-100 text-indigo-700 py-1.5 rounded text-xs transition border border-indigo-200" title="นำมาไว้ข้างหน้า"> |
| 124 | <i class="fa-solid fa-layer-group"></i> ขึ้นบน |
| 125 | </button> |
| 126 | <button id="sendBackwardBtn" class="flex-1 bg-white hover:bg-indigo-100 text-indigo-700 py-1.5 rounded text-xs transition border border-indigo-200" title="ส่งไปข้างหลัง"> |
| 127 | <i class="fa-solid fa-layer-group fa-flip-vertical"></i> ลงล่าง |
| 128 | </button> |
| 129 | <button id="deleteObjBtn" class="flex-1 bg-white hover:bg-red-100 text-red-600 py-1.5 rounded text-xs transition border border-red-200"> |
| 130 | <i class="fa-solid fa-trash"></i> ลบ |
| 131 | </button> |
| 132 | </div> |
| 133 | |
| 134 | <!-- Text Specific Controls --> |
| 135 | <div id="textControls" class="hidden mt-3 pt-3 border-t border-indigo-200"> |
| 136 | <label class="block text-xs font-medium text-indigo-900 mb-2"><i class="fa-solid fa-font"></i> รูปแบบข้อความ</label> |
| 137 | <div class="grid grid-cols-2 gap-2"> |
| 138 | <div> |
| 139 | <label class="text-[10px] text-indigo-700">แบบอักษร</label> |
| 140 | <select id="fontFamilySelect" class="w-full text-xs p-1.5 border border-indigo-200 rounded bg-white"> |
| 141 | <option value="sans-serif">Sans-serif</option> |
| 142 | <option value="serif">Serif</option> |
| 143 | <option value="Arial">Arial</option> |
| 144 | <option value="Kanit">Kanit (ไทย)</option> |
| 145 | <option value="Prompt">Prompt (ไทย)</option> |
| 146 | </select> |
| 147 | </div> |
| 148 | <div> |
| 149 | <label class="text-[10px] text-indigo-700">ขนาด</label> |
| 150 | <input type="number" id="fontSizeInput" min="10" max="200" value="40" class="w-full text-xs p-1.5 border border-indigo-200 rounded bg-white"> |
| 151 | </div> |
| 152 | </div> |
| 153 | </div> |
| 154 | </div> |
| 155 | |
| 156 | <hr class="border-slate-100 my-4"> |
| 157 | |
| 158 | <!-- Filters (Applies to selected image or background) --> |
| 159 | <div class="space-y-3 mb-5"> |
| 160 | <label class="block text-sm font-medium text-slate-700 mb-1"><i class="fa-solid fa-sliders mr-1"></i> ปรับแต่งสี/แสง (ภาพที่เลือก)</label> |
| 161 | <div> |
| 162 | <label class="flex justify-between text-xs text-slate-500 mb-1"> |
| 163 | <span>ความสว่าง</span> <span id="valBright">0</span> |
| 164 | </label> |
| 165 | <input type="range" id="filterBright" min="-100" max="100" value="0" class="w-full h-1.5 bg-slate-200 rounded-lg appearance-none cursor-pointer"> |
| 166 | </div> |
| 167 | <div> |
| 168 | <label class="flex justify-between text-xs text-slate-500 mb-1"> |
| 169 | <span>ความเปรียบต่าง</span> <span id="valContrast">0</span> |
| 170 | </label> |
| 171 | <input type="range" id="filterContrast" min="-100" max="100" value="0" class="w-full h-1.5 bg-slate-200 rounded-lg appearance-none cursor-pointer"> |
| 172 | </div> |
| 173 | <div> |
| 174 | <label class="flex justify-between text-xs text-slate-500 mb-1"> |
| 175 | <span>ความอิ่มตัวสี</span> <span id="valSaturate">0</span> |
| 176 | </label> |
| 177 | <input type="range" id="filterSaturate" min="-100" max="100" value="0" class="w-full h-1.5 bg-slate-200 rounded-lg appearance-none cursor-pointer"> |
| 178 | </div> |
| 179 | <button id="resetFiltersBtn" class="text-xs text-indigo-600 hover:text-indigo-800">รีเซ็ตฟิลเตอร์</button> |
| 180 | </div> |
| 181 | |
| 182 | <hr class="border-slate-100 my-4"> |
| 183 | |
| 184 | <!-- Drawing --> |
| 185 | <div class="mb-5"> |
| 186 | <label class="block text-sm font-medium text-slate-700 mb-2"><i class="fa-solid fa-pen mr-1"></i> วาดเขียนอิสระ</label> |
| 187 | <div class="flex items-center space-x-3 mb-3"> |
| 188 | <input type="color" id="brushColor" value="#ff0000" class="w-8 h-8 rounded cursor-pointer border-0 p-0"> |
| 189 | <input type="range" id="brushSize" min="1" max="50" value="5" class="flex-1 h-1.5 bg-slate-200 rounded-lg appearance-none cursor-pointer"> |
| 190 | </div> |
| 191 | <button id="toggleDrawBtn" class="w-full bg-slate-100 hover:bg-slate-200 text-slate-700 py-2 rounded-lg text-sm font-medium transition border border-slate-300"> |
| 192 | <i class="fa-solid fa-pen-nib mr-1"></i> เปิดโหมดวาดเขียน |
| 193 | </button> |
| 194 | </div> |
| 195 | |
| 196 | <!-- Actions --> |
| 197 | <div class="flex flex-col space-y-2 mt-6"> |
| 198 | <button id="downloadBtn" class="w-full bg-emerald-600 hover:bg-emerald-700 text-white font-medium py-2.5 px-4 rounded-lg transition flex justify-center items-center shadow-sm"> |
| 199 | <i class="fa-solid fa-download mr-2"></i> ดาวน์โหลดผลงาน |
| 200 | </button> |
| 201 | </div> |
| 202 | </div> |
| 203 | </div> |
| 204 | |
| 205 | <!-- Right Column: Canvas Display (Fabric.js) --> |
| 206 | <div class="lg:col-span-8 xl:col-span-9 flex flex-col"> |
| 207 | <!-- Toolbar hint --> |
| 208 | <div class="bg-indigo-50 text-indigo-800 text-sm py-2 px-4 rounded-t-xl border border-indigo-100 flex justify-between"> |
| 209 | <span><i class="fa-solid fa-circle-info mr-1"></i> ดับเบิ้ลคลิกที่ข้อความเพื่อแก้ไข / ลากเพื่อย้ายตำแหน่ง</span> |
| 210 | <span id="canvasDimInfo" class="font-mono text-xs text-indigo-500"></span> |
| 211 | </div> |
| 212 | |
| 213 | <div id="canvasWrapper" class="bg-slate-200 p-2 rounded-b-xl shadow-inner border border-slate-300 min-h-[500px] flex items-center justify-center overflow-auto flex-1 relative bg-[url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMCIgaGVpZ2h0PSIyMCI+CjxyZWN0IHdpZHRoPSIyMCIgaGVpZ2h0PSIyMCIgZmlsbD0iI2ZmZmZmZiIvPgo8cmVjdCB3aWR0aD0iMTAiIGhlaWdodD0iMTAiIGZpbGw9IiNmM2Y0ZjYiLz4KPHJlY3QgeD0iMTAiIHk9IjEwIiB3aWR0aD0iMTAiIGhlaWdodD0iMTAiIGZpbGw9IiNmM2Y0ZjYiLz4KPC9zdmc+')]"> |
| 214 | |
| 215 | <!-- Placeholder text before upload --> |
| 216 | <div id="canvasPlaceholder" class="text-slate-400 text-center pointer-events-none absolute z-10"> |
| 217 | <i class="fa-regular fa-images text-6xl mb-3 opacity-50"></i> |
| 218 | <p>อัปโหลดภาพหลักเพื่อเริ่มสร้างชิ้นงาน</p> |
| 219 | </div> |
| 220 | |
| 221 | <!-- Fabric Canvas Canvas Element --> |
| 222 | <canvas id="mainCanvas"></canvas> |
| 223 | </div> |
| 224 | </div> |
| 225 | |
| 226 | </div> |
| 227 | </div> |
| 228 | |
| 229 | <script> |
| 230 | // --- Fabric.js Initialization --- |
| 231 | const canvas = new fabric.Canvas('mainCanvas', { |
| 232 | isDrawingMode: false, |
| 233 | preserveObjectStacking: true // Keep objects in order when selected |
| 234 | }); |
| 235 | |
| 236 | let baseImage = null; // The background image uploaded first |
| 237 | |
| 238 | // --- DOM Elements --- |
| 239 | const imageUpload = document.getElementById('imageUpload'); |
| 240 | const detectionPanel = document.getElementById('detectionPanel'); |
| 241 | const editorPanel = document.getElementById('editorPanel'); |
| 242 | const canvasPlaceholder = document.getElementById('canvasPlaceholder'); |
| 243 | const canvasDimInfo = document.getElementById('canvasDimInfo'); |
| 244 | |
| 245 | // Layers & Objects controls |
| 246 | const addTextBtn = document.getElementById('addTextBtn'); |
| 247 | const addLayerImage = document.getElementById('addLayerImage'); |
| 248 | const addRectBtn = document.getElementById('addRectBtn'); |
| 249 | const addCircleBtn = document.getElementById('addCircleBtn'); |
| 250 | const magicTextBtn = document.getElementById('magicTextBtn'); |
| 251 | const objectControls = document.getElementById('objectControls'); |
| 252 | const objColorPicker = document.getElementById('objColorPicker'); |
| 253 | const bringForwardBtn = document.getElementById('bringForwardBtn'); |
| 254 | const sendBackwardBtn = document.getElementById('sendBackwardBtn'); |
| 255 | const deleteObjBtn = document.getElementById('deleteObjBtn'); |
| 256 | const textControls = document.getElementById('textControls'); |
| 257 | const fontFamilySelect = document.getElementById('fontFamilySelect'); |
| 258 | const fontSizeInput = document.getElementById('fontSizeInput'); |
| 259 | |
| 260 | // Filter Controls |
| 261 | const filterBright = document.getElementById('filterBright'); |
| 262 | const filterContrast = document.getElementById('filterContrast'); |
| 263 | const filterSaturate = document.getElementById('filterSaturate'); |
| 264 | const resetFiltersBtn = document.getElementById('resetFiltersBtn'); |
| 265 | |
| 266 | // Drawing Controls |
| 267 | const toggleDrawBtn = document.getElementById('toggleDrawBtn'); |
| 268 | const brushColor = document.getElementById('brushColor'); |
| 269 | const brushSize = document.getElementById('brushSize'); |
| 270 | |
| 271 | // Action |
| 272 | const downloadBtn = document.getElementById('downloadBtn'); |
| 273 | |
| 274 | // Magic Text Variables |
| 275 | let isMagicTextMode = false; |
| 276 | let magicRect = null; |
| 277 | let origX, origY; |
| 278 | |
| 279 | // --- 1. Load Main Image --- |
| 280 | imageUpload.addEventListener('change', function(e) { |
| 281 | const file = e.target.files[0]; |
| 282 | if (!file) return; |
| 283 | |
| 284 | const reader = new FileReader(); |
| 285 | reader.onload = function(event) { |
| 286 | // Remove placeholder |
| 287 | canvasPlaceholder.classList.add('hidden'); |
| 288 | |
| 289 | fabric.Image.fromURL(event.target.result, function(img) { |
| 290 | // Set canvas dimensions |
| 291 | // Limit max width to prevent extremely large canvases in browser, scale proportionally |
| 292 | const MAX_WIDTH = 1000; |
| 293 | let scale = 1; |
| 294 | if (img.width > MAX_WIDTH) { |
| 295 | scale = MAX_WIDTH / img.width; |
| 296 | } |
| 297 | |
| 298 | const finalWidth = img.width * scale; |
| 299 | const finalHeight = img.height * scale; |
| 300 | |
| 301 | canvas.setWidth(finalWidth); |
| 302 | canvas.setHeight(finalHeight); |
| 303 | |
| 304 | // Scale image |
| 305 | img.scale(scale); |
| 306 | |
| 307 | // Set as background image so it acts as the base canvas |
| 308 | canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas)); |
| 309 | baseImage = img; // Store reference for filters |
| 310 | |
| 311 | canvasDimInfo.innerText = `${Math.round(finalWidth)} x ${Math.round(finalHeight)} px`; |
| 312 | |
| 313 | // Trigger fake detection |
| 314 | startDetectionSimulation(); |
| 315 | }); |
| 316 | } |
| 317 | reader.readAsDataURL(file); |
| 318 | }); |
| 319 | |
| 320 | // --- 2. Element / Layer Additions (Canva style) --- |
| 321 | addTextBtn.addEventListener('click', () => { |
| 322 | const text = new fabric.IText('ข้อความใหม่', { |
| 323 | left: canvas.width / 2, |
| 324 | top: canvas.height / 2, |
| 325 | originX: 'center', |
| 326 | originY: 'center', |
| 327 | fontFamily: 'sans-serif', |
| 328 | fill: '#333333', |
| 329 | fontSize: 40, |
| 330 | fontWeight: 'bold', |
| 331 | transparentCorners: false, |
| 332 | cornerColor: 'blue', |
| 333 | cornerSize: 10 |
| 334 | }); |
| 335 | canvas.add(text); |
| 336 | canvas.setActiveObject(text); |
| 337 | }); |
| 338 | |
| 339 | addRectBtn.addEventListener('click', () => { |
| 340 | const rect = new fabric.Rect({ |
| 341 | left: canvas.width / 2, |
| 342 | top: canvas.height / 2, |
| 343 | originX: 'center', |
| 344 | originY: 'center', |
| 345 | fill: '#4f46e5', |
| 346 | width: 100, |
| 347 | height: 100, |
| 348 | rx: 10, |
| 349 | ry: 10, |
| 350 | transparentCorners: false, |
| 351 | cornerColor: 'blue', |
| 352 | cornerSize: 10 |
| 353 | }); |
| 354 | canvas.add(rect); |
| 355 | canvas.setActiveObject(rect); |
| 356 | }); |
| 357 | |
| 358 | addCircleBtn.addEventListener('click', () => { |
| 359 | const circle = new fabric.Circle({ |
| 360 | left: canvas.width / 2, |
| 361 | top: canvas.height / 2, |
| 362 | originX: 'center', |
| 363 | originY: 'center', |
| 364 | fill: '#10b981', |
| 365 | radius: 50, |
| 366 | transparentCorners: false, |
| 367 | cornerColor: 'blue', |
| 368 | cornerSize: 10 |
| 369 | }); |
| 370 | canvas.add(circle); |
| 371 | canvas.setActiveObject(circle); |
| 372 | }); |
| 373 | |
| 374 | addLayerImage.addEventListener('change', function(e) { |
| 375 | const file = e.target.files[0]; |
| 376 | if (!file) return; |
| 377 | const reader = new FileReader(); |
| 378 | reader.onload = function(event) { |
| 379 | fabric.Image.fromURL(event.target.result, function(img) { |
| 380 | // scale down if it's too big for the canvas |
| 381 | if(img.width > canvas.width / 2) { |
| 382 | img.scaleToWidth(canvas.width / 2); |
| 383 | } |
| 384 | img.set({ |
| 385 | left: canvas.width / 2, |
| 386 | top: canvas.height / 2, |
| 387 | originX: 'center', |
| 388 | originY: 'center', |
| 389 | transparentCorners: false, |
| 390 | cornerColor: 'blue', |
| 391 | cornerSize: 10 |
| 392 | }); |
| 393 | canvas.add(img); |
| 394 | canvas.setActiveObject(img); |
| 395 | }); |
| 396 | } |
| 397 | reader.readAsDataURL(file); |
| 398 | this.value = ''; // Reset input |
| 399 | }); |
| 400 | |
| 401 | // --- 3. Manage Object State (Enable/Disable tools) --- |
| 402 | function updateObjectControls() { |
| 403 | const activeObj = canvas.getActiveObject(); |
| 404 | if (activeObj) { |
| 405 | // Enable layer tools |
| 406 | objectControls.classList.remove('opacity-50', 'pointer-events-none'); |
| 407 | |
| 408 | // Show/hide text controls |
| 409 | if (activeObj.type === 'i-text' || activeObj.type === 'text') { |
| 410 | textControls.classList.remove('hidden'); |
| 411 | fontFamilySelect.value = activeObj.fontFamily || 'sans-serif'; |
| 412 | fontSizeInput.value = activeObj.fontSize || 40; |
| 413 | } else { |
| 414 | textControls.classList.add('hidden'); |
| 415 | } |
| 416 | |
| 417 | // Update filter sliders if an image is selected |
| 418 | if (activeObj.type === 'image') { |
| 419 | updateFilterSlidersFromObject(activeObj); |
| 420 | objColorPicker.classList.add('hidden'); |
| 421 | } else { |
| 422 | // Show color picker for text and shapes |
| 423 | objColorPicker.classList.remove('hidden'); |
| 424 | if (activeObj.fill && typeof activeObj.fill === 'string') { |
| 425 | if (activeObj.fill.startsWith('#')) { |
| 426 | objColorPicker.value = activeObj.fill.substring(0, 7); |
| 427 | } else if (activeObj.fill.startsWith('rgb')) { |
| 428 | const rgb = activeObj.fill.match(/\d+/g); |
| 429 | if(rgb && rgb.length >= 3) { |
| 430 | const hex = "#" + (1 << 24 | rgb[0] << 16 | rgb[1] << 8 | rgb[2]).toString(16).slice(1); |
| 431 | objColorPicker.value = hex; |
| 432 | } |
| 433 | } |
| 434 | } |
| 435 | } |
| 436 | } else { |
| 437 | // Disable layer tools |
| 438 | objectControls.classList.add('opacity-50', 'pointer-events-none'); |
| 439 | objColorPicker.classList.add('hidden'); |
| 440 | textControls.classList.add('hidden'); |
| 441 | |
| 442 | // If nothing selected, show filters of the background image |
| 443 | if (baseImage) { |
| 444 | updateFilterSlidersFromObject(baseImage); |
| 445 | } |
| 446 | } |
| 447 | } |
| 448 | |
| 449 | canvas.on('selection:created', updateObjectControls); |
| 450 | canvas.on('selection:updated', updateObjectControls); |
| 451 | canvas.on('selection:cleared', updateObjectControls); |
| 452 | |
| 453 | objColorPicker.addEventListener('input', (e) => { |
| 454 | const activeObj = canvas.getActiveObject(); |
| 455 | if (activeObj) { |
| 456 | activeObj.set('fill', e.target.value); |
| 457 | canvas.renderAll(); |
| 458 | } |
| 459 | }); |
| 460 | |
| 461 | deleteObjBtn.addEventListener('click', () => { |
| 462 | const activeObjects = canvas.getActiveObjects(); |
| 463 | if (activeObjects.length) { |
| 464 | activeObjects.forEach(obj => canvas.remove(obj)); |
| 465 | canvas.discardActiveObject(); |
| 466 | } |
| 467 | }); |
| 468 | |
| 469 | fontFamilySelect.addEventListener('change', (e) => { |
| 470 | const activeObj = canvas.getActiveObject(); |
| 471 | if (activeObj && (activeObj.type === 'i-text' || activeObj.type === 'text')) { |
| 472 | activeObj.set('fontFamily', e.target.value); |
| 473 | canvas.renderAll(); |
| 474 | } |
| 475 | }); |
| 476 | |
| 477 | fontSizeInput.addEventListener('input', (e) => { |
| 478 | const activeObj = canvas.getActiveObject(); |
| 479 | if (activeObj && (activeObj.type === 'i-text' || activeObj.type === 'text')) { |
| 480 | const size = parseInt(e.target.value, 10); |
| 481 | if (!isNaN(size) && size > 0) { |
| 482 | activeObj.set('fontSize', size); |
| 483 | canvas.renderAll(); |
| 484 | } |
| 485 | } |
| 486 | }); |
| 487 | |
| 488 | bringForwardBtn.addEventListener('click', () => { |
| 489 | const activeObj = canvas.getActiveObject(); |
| 490 | if (activeObj) canvas.bringForward(activeObj); |
| 491 | }); |
| 492 | |
| 493 | sendBackwardBtn.addEventListener('click', () => { |
| 494 | const activeObj = canvas.getActiveObject(); |
| 495 | if (activeObj) canvas.sendBackwards(activeObj); |
| 496 | }); |
| 497 | |
| 498 | // --- 3.5 Magic Text Editor (Masking Old Text & Replace) --- |
| 499 | magicTextBtn.addEventListener('click', () => { |
| 500 | isMagicTextMode = true; |
| 501 | canvas.isDrawingMode = false; |
| 502 | canvas.defaultCursor = 'crosshair'; |
| 503 | canvas.discardActiveObject(); |
| 504 | canvas.renderAll(); |
| 505 | |
| 506 | // Highlight button to show it's active |
| 507 | magicTextBtn.classList.replace('from-purple-500', 'from-pink-500'); |
| 508 | magicTextBtn.classList.replace('to-indigo-500', 'to-rose-500'); |
| 509 | magicTextBtn.innerHTML = '<i class="fa-solid fa-check mr-1"></i> ลากคลุมข้อความบนภาพเลย...'; |
| 510 | }); |
| 511 | |
| 512 | canvas.on('mouse:down', function(o) { |
| 513 | if (!isMagicTextMode) return; |
| 514 | var pointer = canvas.getPointer(o.e); |
| 515 | origX = pointer.x; |
| 516 | origY = pointer.y; |
| 517 | |
| 518 | magicRect = new fabric.Rect({ |
| 519 | left: origX, |
| 520 | top: origY, |
| 521 | width: 0, |
| 522 | height: 0, |
| 523 | fill: 'rgba(236, 72, 153, 0.3)', // Pinkish selection box |
| 524 | stroke: '#ec4899', |
| 525 | strokeWidth: 2, |
| 526 | selectable: false |
| 527 | }); |
| 528 | canvas.add(magicRect); |
| 529 | }); |
| 530 | |
| 531 | canvas.on('mouse:move', function(o) { |
| 532 | if (!isMagicTextMode || !magicRect) return; |
| 533 | var pointer = canvas.getPointer(o.e); |
| 534 | |
| 535 | if (origX > pointer.x) { |
| 536 | magicRect.set({ left: Math.abs(pointer.x) }); |
| 537 | } |
| 538 | if (origY > pointer.y) { |
| 539 | magicRect.set({ top: Math.abs(pointer.y) }); |
| 540 | } |
| 541 | |
| 542 | magicRect.set({ width: Math.abs(origX - pointer.x) }); |
| 543 | magicRect.set({ height: Math.abs(origY - pointer.y) }); |
| 544 | canvas.renderAll(); |
| 545 | }); |
| 546 | |
| 547 | canvas.on('mouse:up', function(o) { |
| 548 | if (!isMagicTextMode || !magicRect) return; |
| 549 | |
| 550 | // Ensure selection is big enough |
| 551 | if (magicRect.width < 10 || magicRect.height < 10) { |
| 552 | canvas.remove(magicRect); |
| 553 | resetMagicMode(); |
| 554 | return; |
| 555 | } |
| 556 | |
| 557 | // 1. Hide selection box temporarily to read actual background pixels |
| 558 | magicRect.set('visible', false); |
| 559 | canvas.renderAll(); |
| 560 | |
| 561 | // 2. Read background color from the Top-Left of the selection |
| 562 | const ctx = canvas.getContext('2d'); |
| 563 | const multiplier = canvas.getRetinaScaling(); |
| 564 | const pixel = ctx.getImageData(magicRect.left * multiplier, magicRect.top * multiplier, 1, 1).data; |
| 565 | const bgColor = `rgb(${pixel[0]}, ${pixel[1]}, ${pixel[2]})`; |
| 566 | |
| 567 | // 3. Make the box visible again, filled with background color to "Erase" old text |
| 568 | magicRect.set({ |
| 569 | visible: true, |
| 570 | fill: bgColor, |
| 571 | strokeWidth: 0, |
| 572 | selectable: true // allow user to resize mask later if needed |
| 573 | }); |
| 574 | |
| 575 | // 4. Create new editable text over the erased area |
| 576 | const text = new fabric.IText('ข้อความใหม่', { |
| 577 | left: magicRect.left + (magicRect.width * 0.05), |
| 578 | top: magicRect.top + (magicRect.height * 0.1), |
| 579 | fontFamily: 'sans-serif', |
| 580 | fill: '#333333', // Default text color |
| 581 | fontSize: Math.max(16, magicRect.height * 0.6), |
| 582 | fontWeight: 'bold', |
| 583 | transparentCorners: false, |
| 584 | cornerColor: 'blue', |
| 585 | cornerSize: 10 |
| 586 | }); |
| 587 | canvas.add(text); |
| 588 | |
| 589 | // 5. Select the new text and open edit mode automatically |
| 590 | canvas.setActiveObject(text); |
| 591 | text.enterEditing(); |
| 592 | text.selectAll(); |
| 593 | |
| 594 | resetMagicMode(); |
| 595 | }); |
| 596 | |
| 597 | function resetMagicMode() { |
| 598 | isMagicTextMode = false; |
| 599 | magicRect = null; |
| 600 | canvas.defaultCursor = 'default'; |
| 601 | magicTextBtn.classList.replace('from-pink-500', 'from-purple-500'); |
| 602 | magicTextBtn.classList.replace('to-rose-500', 'to-indigo-500'); |
| 603 | magicTextBtn.innerHTML = '<i class="fa-solid fa-wand-magic-sparkles mr-1"></i> แก้ไขข้อความเดิมบนภาพ (Magic Edit)'; |
| 604 | canvas.renderAll(); |
| 605 | } |
| 606 | |
| 607 | // --- 4. Filters (Fabric Image Filters) --- |
| 608 | // Range slider value is -100 to 100. Fabric expects -1 to 1. |
| 609 | function applyImageFilters() { |
| 610 | // Apply to active image object, or background baseImage if none selected |
| 611 | const target = canvas.getActiveObject() && canvas.getActiveObject().type === 'image' |
| 612 | ? canvas.getActiveObject() |
| 613 | : baseImage; |
| 614 | |
| 615 | if (!target) return; |
| 616 | |
| 617 | document.getElementById('valBright').innerText = filterBright.value; |
| 618 | document.getElementById('valContrast').innerText = filterContrast.value; |
| 619 | document.getElementById('valSaturate').innerText = filterSaturate.value; |
| 620 | |
| 621 | // Fabric 5 filter API |
| 622 | target.filters[0] = new fabric.Image.filters.Brightness({ brightness: parseFloat(filterBright.value) / 100 }); |
| 623 | target.filters[1] = new fabric.Image.filters.Contrast({ contrast: parseFloat(filterContrast.value) / 100 }); |
| 624 | target.filters[2] = new fabric.Image.filters.Saturation({ saturation: parseFloat(filterSaturate.value) / 100 }); |
| 625 | |
| 626 | target.applyFilters(); |
| 627 | canvas.renderAll(); |
| 628 | } |
| 629 | |
| 630 | function updateFilterSlidersFromObject(obj) { |
| 631 | if (!obj.filters || obj.filters.length === 0) { |
| 632 | filterBright.value = 0; filterContrast.value = 0; filterSaturate.value = 0; |
| 633 | } else { |
| 634 | filterBright.value = (obj.filters[0] && obj.filters[0].brightness) ? Math.round(obj.filters[0].brightness * 100) : 0; |
| 635 | filterContrast.value = (obj.filters[1] && obj.filters[1].contrast) ? Math.round(obj.filters[1].contrast * 100) : 0; |
| 636 | filterSaturate.value = (obj.filters[2] && obj.filters[2].saturation) ? Math.round(obj.filters[2].saturation * 100) : 0; |
| 637 | } |
| 638 | document.getElementById('valBright').innerText = filterBright.value; |
| 639 | document.getElementById('valContrast').innerText = filterContrast.value; |
| 640 | document.getElementById('valSaturate').innerText = filterSaturate.value; |
| 641 | } |
| 642 | |
| 643 | filterBright.addEventListener('input', applyImageFilters); |
| 644 | filterContrast.addEventListener('input', applyImageFilters); |
| 645 | filterSaturate.addEventListener('input', applyImageFilters); |
| 646 | |
| 647 | resetFiltersBtn.addEventListener('click', () => { |
| 648 | filterBright.value = 0; filterContrast.value = 0; filterSaturate.value = 0; |
| 649 | applyImageFilters(); |
| 650 | }); |
| 651 | |
| 652 | // --- 5. Free Drawing --- |
| 653 | // Setup brush |
| 654 | canvas.freeDrawingBrush = new fabric.PencilBrush(canvas); |
| 655 | canvas.freeDrawingBrush.color = brushColor.value; |
| 656 | canvas.freeDrawingBrush.width = parseInt(brushSize.value, 10) || 5; |
| 657 | |
| 658 | brushColor.addEventListener('change', (e) => { |
| 659 | canvas.freeDrawingBrush.color = e.target.value; |
| 660 | }); |
| 661 | |
| 662 | brushSize.addEventListener('input', (e) => { |
| 663 | canvas.freeDrawingBrush.width = parseInt(e.target.value, 10); |
| 664 | }); |
| 665 | |
| 666 | toggleDrawBtn.addEventListener('click', () => { |
| 667 | canvas.isDrawingMode = !canvas.isDrawingMode; |
| 668 | if (canvas.isDrawingMode) { |
| 669 | toggleDrawBtn.classList.replace('bg-slate-100', 'bg-indigo-600'); |
| 670 | toggleDrawBtn.classList.replace('text-slate-700', 'text-white'); |
| 671 | canvas.discardActiveObject(); |
| 672 | canvas.requestRenderAll(); |
| 673 | } else { |
| 674 | toggleDrawBtn.classList.replace('bg-indigo-600', 'bg-slate-100'); |
| 675 | toggleDrawBtn.classList.replace('text-white', 'text-slate-700'); |
| 676 | } |
| 677 | }); |
| 678 | |
| 679 | // --- 6. Download --- |
| 680 | downloadBtn.addEventListener('click', () => { |
| 681 | // Unselect everything before export so bounding boxes aren't visible |
| 682 | canvas.discardActiveObject(); |
| 683 | canvas.renderAll(); |
| 684 | |
| 685 | const link = document.createElement('a'); |
| 686 | link.download = 'ai-canvas-edited.png'; |
| 687 | link.href = canvas.toDataURL({ format: 'png', quality: 1 }); |
| 688 | link.click(); |
| 689 | }); |
| 690 | |
| 691 | // --- 7. Detection Simulation --- |
| 692 | const startEditBtn = document.getElementById('startEditBtn'); |
| 693 | startEditBtn.addEventListener('click', () => { |
| 694 | detectionPanel.classList.add('hidden'); |
| 695 | editorPanel.classList.remove('hidden'); |
| 696 | }); |
| 697 | |
| 698 | function startDetectionSimulation() { |
| 699 | editorPanel.classList.add('hidden'); |
| 700 | detectionPanel.classList.remove('hidden'); |
| 701 | document.getElementById('detectionLoading').classList.remove('hidden'); |
| 702 | document.getElementById('detectionResult').classList.add('hidden'); |
| 703 | |
| 704 | const loadingBar = document.getElementById('loadingBar'); |
| 705 | const loadingText = document.getElementById('loadingText'); |
| 706 | loadingBar.style.width = '0%'; |
| 707 | |
| 708 | setTimeout(() => { loadingBar.style.width = '30%'; loadingText.innerText = 'ตรวจสอบ Noise Pattern...'; }, 800); |
| 709 | setTimeout(() => { loadingBar.style.width = '60%'; loadingText.innerText = 'วิเคราะห์ขอบเขตเลเยอร์ (Edge Analysis)...'; }, 1600); |
| 710 | setTimeout(() => { loadingBar.style.width = '90%'; loadingText.innerText = 'ประมวลผลผลลัพธ์...'; }, 2400); |
| 711 | |
| 712 | setTimeout(() => { |
| 713 | showDetectionResult(); |
| 714 | }, 3000); |
| 715 | } |
| 716 | |
| 717 | function showDetectionResult() { |
| 718 | document.getElementById('detectionLoading').classList.add('hidden'); |
| 719 | document.getElementById('detectionResult').classList.remove('hidden'); |
| 720 | |
| 721 | const isLikelyAI = Math.random() > 0.4; |
| 722 | const resultIcon = document.getElementById('resultIcon'); |
| 723 | const resultTitle = document.getElementById('resultTitle'); |
| 724 | const resultDesc = document.getElementById('resultDesc'); |
| 725 | const scoreAi = document.getElementById('scoreAi'); |
| 726 | const scoreNoise = document.getElementById('scoreNoise'); |
| 727 | |
| 728 | if (isLikelyAI) { |
| 729 | resultIcon.innerHTML = '<i class="fa-solid fa-robot text-orange-500"></i>'; |
| 730 | resultTitle.innerText = 'โอกาสสูงที่เป็นภาพสร้างจาก AI'; |
| 731 | resultTitle.className = 'text-lg font-bold mb-1 text-orange-600'; |
| 732 | resultDesc.innerText = 'พบร่องรอยการกระจายพิกเซลผิดปกติ (Generative Artifacts)'; |
| 733 | scoreAi.innerText = (85 + Math.floor(Math.random() * 14)) + '%'; |
| 734 | scoreAi.className = 'font-semibold text-orange-600'; |
| 735 | scoreNoise.innerText = 'สังเคราะห์ (Synthetic)'; |
| 736 | } else { |
| 737 | resultIcon.innerHTML = '<i class="fa-solid fa-camera text-emerald-500"></i>'; |
| 738 | resultTitle.innerText = 'น่าจะเป็นภาพถ่ายจริง'; |
| 739 | resultTitle.className = 'text-lg font-bold mb-1 text-emerald-600'; |
| 740 | resultDesc.innerText = 'ไม่พบร่องรอยการสังเคราะห์ของ AI ในระดับที่ผิดสังเกต'; |
| 741 | scoreAi.innerText = (2 + Math.floor(Math.random() * 20)) + '%'; |
| 742 | scoreAi.className = 'font-semibold text-emerald-600'; |
| 743 | scoreNoise.innerText = 'ปกติ (Natural)'; |
| 744 | } |
| 745 | } |
| 746 | </script> |
| 747 | </body> |
| 748 | </html> |
Copied to clipboard!
Paste Info
- ID
- bX5nLd
- Type
- Text Paste
- Size
- 37.7 KB
- Lines
- 748
- Views
- 9
- Created
- 1h ago
Report This Paste
\n \n \n \n \n\n \n
\n\n \n\n\n AI Image Detector & Editor\n
\nตรวจสอบภาพ AI และระบบแก้ไขภาพแบบเลเยอร์ (Canva Style)
\n\n \n \n
\n \n \n \n
\n\n \n \n
\n\n \n 1. ภาพหลัก (Background)
\n \n\n
\n\n \n 2. ผลการวิเคราะห์ภาพ
\n \n\n \n
\n\n กำลังตรวจสอบ Metadata...
\n\n \n
\n \n \n \n \n \n
\n \n
\n \n \n โอกาสเป็นภาพ AI:
\n Noise Pattern:
\n \n
\n\n \n
\n\n \n
\n 3. เครื่องมือแก้ไข (Canva Style)
\n \n \n\n \n
\n\n \n \n \n \n \n \n
\n \n \n \n \n
\n คลิกปุ่ม แล้วลากคลุมข้อความเดิม เพื่อลบและพิมพ์ใหม่
\n\n
\n\n \n \n \n
\n \n \n \n \n
\n \n \n \n \n
\n \n
\n \n \n \n
\n \n \n \n
\n \n\n \n
\n \n
\n\n \n \n \n
\n \n \n \n
\n \n \n \n
\n \n \n\n \n
\n \n
\n\n \n \n \n \n
\n \n \n \n
\n \n \n
\n \n \n ดับเบิ้ลคลิกที่ข้อความเพื่อแก้ไข / ลากเพื่อย้ายตำแหน่ง\n \n
\n \n \n \n \n
\n \n \n
\n\n \n \n อัปโหลดภาพหลักเพื่อเริ่มสร้างชิ้นงาน
\n