|
302 | 302 | font-size: 0.875rem; |
303 | 303 | text-align: center; |
304 | 304 | } |
| 305 | + |
| 306 | + .bg-removal-section { |
| 307 | + border-top: 1px solid #4b5563; |
| 308 | + padding-top: 1rem; |
| 309 | + margin-bottom: 1.5rem; |
| 310 | + } |
| 311 | + |
| 312 | + .bg-removal-toggle-row { |
| 313 | + display: flex; |
| 314 | + align-items: center; |
| 315 | + justify-content: space-between; |
| 316 | + cursor: pointer; |
| 317 | + } |
| 318 | + .bg-removal-toggle-row .controls-subheading { margin: 0; } |
| 319 | + .bg-removal-toggle-row .expand-arrow { |
| 320 | + font-size: 0.75rem; |
| 321 | + color: #9ca3af; |
| 322 | + transition: transform 0.2s; |
| 323 | + user-select: none; |
| 324 | + } |
| 325 | + .bg-removal-toggle-row .expand-arrow.open { |
| 326 | + transform: rotate(90deg); |
| 327 | + } |
| 328 | + |
| 329 | + .bg-removal-body { |
| 330 | + overflow: hidden; |
| 331 | + max-height: 0; |
| 332 | + transition: max-height 0.3s ease; |
| 333 | + } |
| 334 | + .bg-removal-body.open { |
| 335 | + max-height: 500px; |
| 336 | + } |
| 337 | + .bg-removal-body-inner { |
| 338 | + padding-top: 0.75rem; |
| 339 | + } |
| 340 | + .bg-removal-body-inner > * + * { margin-top: 0.75rem; } |
| 341 | + |
| 342 | + .controls-subheading { |
| 343 | + font-size: 1rem; |
| 344 | + font-weight: 600; |
| 345 | + color: white; |
| 346 | + } |
| 347 | + |
| 348 | + .bg-removal-warning { |
| 349 | + font-size: 0.75rem; |
| 350 | + color: #fbbf24; |
| 351 | + line-height: 1.4; |
| 352 | + } |
| 353 | + |
| 354 | + .color-picker-row { |
| 355 | + display: flex; |
| 356 | + align-items: center; |
| 357 | + gap: 0.5rem; |
| 358 | + } |
| 359 | + .color-picker-row .slider-label { flex: 1; } |
| 360 | + |
| 361 | + .color-picker-row input[type="color"] { |
| 362 | + width: 2.5rem; |
| 363 | + height: 2rem; |
| 364 | + border: 1px solid #6b7280; |
| 365 | + border-radius: 0.25rem; |
| 366 | + background: none; |
| 367 | + cursor: pointer; |
| 368 | + padding: 0; |
| 369 | + } |
| 370 | + |
| 371 | + .eyedropper-button { |
| 372 | + width: 2rem; |
| 373 | + height: 2rem; |
| 374 | + border: 1px solid #6b7280; |
| 375 | + border-radius: 0.25rem; |
| 376 | + background-color: #4b5563; |
| 377 | + color: #d1d5db; |
| 378 | + cursor: pointer; |
| 379 | + display: flex; |
| 380 | + align-items: center; |
| 381 | + justify-content: center; |
| 382 | + padding: 0; |
| 383 | + transition: background-color 0.2s; |
| 384 | + flex-shrink: 0; |
| 385 | + } |
| 386 | + .eyedropper-button:hover { background-color: #6b7280; } |
| 387 | + .eyedropper-button.picking { |
| 388 | + background-color: #d97706; |
| 389 | + border-color: #f59e0b; |
| 390 | + color: white; |
| 391 | + } |
| 392 | + .eyedropper-button svg { |
| 393 | + width: 1rem; |
| 394 | + height: 1rem; |
| 395 | + } |
| 396 | + |
| 397 | + .pick-color-hint { |
| 398 | + font-size: 0.75rem; |
| 399 | + color: #9ca3af; |
| 400 | + font-style: italic; |
| 401 | + margin-top: 0.125rem !important; |
| 402 | + } |
| 403 | + |
| 404 | + .bg-removal-buttons > * + * { margin-top: 0.5rem; } |
| 405 | + |
| 406 | + .bg-remove-button, .bg-reset-button { |
| 407 | + width: 100%; |
| 408 | + cursor: pointer; |
| 409 | + padding: 0.5rem 1rem; |
| 410 | + border-radius: 0.5rem; |
| 411 | + font-weight: 500; |
| 412 | + border: none; |
| 413 | + font-size: 0.875rem; |
| 414 | + transition: background-color 0.2s; |
| 415 | + } |
| 416 | + |
| 417 | + .bg-remove-button { |
| 418 | + background-color: #d97706; |
| 419 | + color: white; |
| 420 | + } |
| 421 | + .bg-remove-button:hover { background-color: #b45309; } |
| 422 | + |
| 423 | + .bg-reset-button { |
| 424 | + background-color: #6b7280; |
| 425 | + color: white; |
| 426 | + } |
| 427 | + .bg-reset-button:hover { background-color: #4b5563; } |
305 | 428 | </style> |
306 | 429 | </head> |
307 | 430 |
|
@@ -372,6 +495,40 @@ <h2 class="controls-heading">Controls & Results</h2> |
372 | 495 | </div> |
373 | 496 | </div> |
374 | 497 |
|
| 498 | + <!-- Background Removal --> |
| 499 | + <div class="bg-removal-section"> |
| 500 | + <div class="bg-removal-toggle-row" id="bgRemovalToggle"> |
| 501 | + <h3 class="controls-subheading">Background Removal</h3> |
| 502 | + <span class="expand-arrow" id="bgRemovalArrow">▶</span> |
| 503 | + </div> |
| 504 | + <div class="bg-removal-body" id="bgRemovalBody"> |
| 505 | + <div class="bg-removal-body-inner"> |
| 506 | + <p class="bg-removal-warning">⚠ For best results, upload a natively transparent image. Color-based removal is approximate.</p> |
| 507 | + <div class="color-picker-row"> |
| 508 | + <span class="slider-label" style="margin-bottom: 0;">Color to remove</span> |
| 509 | + <button type="button" id="eyedropperButton" class="eyedropper-button" title="Pick color from image"> |
| 510 | + <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
| 511 | + <path d="M2 22l1-1h3l9-9"/> |
| 512 | + <path d="M3 21v-3l9-9"/> |
| 513 | + <path d="M14.5 5.5l4 4"/> |
| 514 | + <path d="M18.5 1.5a2.121 2.121 0 0 1 3 3L16 10l-4-4 5.5-5.5z"/> |
| 515 | + </svg> |
| 516 | + </button> |
| 517 | + <input type="color" id="bgColorPicker" value="#ffffff"> |
| 518 | + </div> |
| 519 | + <p class="pick-color-hint">Use the eyedropper or click on the image to pick a color.</p> |
| 520 | + <div> |
| 521 | + <label for="toleranceSlider" class="slider-label">Tolerance (<span id="toleranceValue">50</span>)</label> |
| 522 | + <input id="toleranceSlider" type="range" min="0" max="255" value="50" class="slider"> |
| 523 | + </div> |
| 524 | + <div class="bg-removal-buttons"> |
| 525 | + <button id="removeBgButton" class="bg-remove-button">Remove Background</button> |
| 526 | + <button id="resetImageButton" class="bg-reset-button hidden">Reset to Original</button> |
| 527 | + </div> |
| 528 | + </div> |
| 529 | + </div> |
| 530 | + </div> |
| 531 | + |
375 | 532 | <div id="results-text" class="results-section"> |
376 | 533 | <p class="results-note">Coordinates are relative to the top-left corner of the |
377 | 534 | image.</p> |
@@ -438,19 +595,30 @@ <h3>No image selected</h3> |
438 | 595 |
|
439 | 596 | const noAlphaWarning = document.getElementById('noAlphaWarning'); |
440 | 597 |
|
| 598 | + const bgColorPicker = document.getElementById('bgColorPicker'); |
| 599 | + const toleranceSlider = document.getElementById('toleranceSlider'); |
| 600 | + const toleranceValueEl = document.getElementById('toleranceValue'); |
| 601 | + const removeBgButton = document.getElementById('removeBgButton'); |
| 602 | + const resetImageButton = document.getElementById('resetImageButton'); |
| 603 | + const eyedropperButton = document.getElementById('eyedropperButton'); |
| 604 | + let eyedropperActive = false; |
| 605 | + |
441 | 606 | let currentImageState = { img: null, centroid: null, bboxCenter: null }; |
| 607 | + let originalImg = null; |
442 | 608 |
|
443 | 609 | imageUpload.addEventListener('change', (event) => handleFile(event.target.files[0])); |
444 | 610 | saveButton.addEventListener('click', saveImageWithMarkers); |
445 | 611 |
|
446 | 612 | // --- Drag and Drop Logic --- |
447 | 613 | const dropZone = document.getElementById('mainCard'); |
448 | 614 | dropZone.addEventListener('dragover', (e) => { |
| 615 | + if (!e.dataTransfer || !e.dataTransfer.types.includes('Files')) return; |
449 | 616 | e.preventDefault(); |
450 | 617 | e.stopPropagation(); |
451 | 618 | dropZone.classList.add('drag-over'); |
452 | 619 | }); |
453 | 620 | dropZone.addEventListener('dragleave', (e) => { |
| 621 | + if (!e.dataTransfer || !e.dataTransfer.types.includes('Files')) return; |
454 | 622 | e.preventDefault(); |
455 | 623 | e.stopPropagation(); |
456 | 624 | dropZone.classList.remove('drag-over'); |
@@ -488,12 +656,72 @@ <h3>No image selected</h3> |
488 | 656 | bboxToggle.addEventListener('change', updateVisualVisibility); |
489 | 657 | crosshairsToggle.addEventListener('change', updateVisualVisibility); |
490 | 658 |
|
| 659 | + // --- Background Removal --- |
| 660 | + const bgRemovalToggle = document.getElementById('bgRemovalToggle'); |
| 661 | + const bgRemovalArrow = document.getElementById('bgRemovalArrow'); |
| 662 | + const bgRemovalBody = document.getElementById('bgRemovalBody'); |
| 663 | + |
| 664 | + bgRemovalToggle.addEventListener('click', () => { |
| 665 | + bgRemovalBody.classList.toggle('open'); |
| 666 | + bgRemovalArrow.classList.toggle('open'); |
| 667 | + }); |
| 668 | + |
| 669 | + toleranceSlider.addEventListener('input', () => { |
| 670 | + toleranceValueEl.textContent = toleranceSlider.value; |
| 671 | + }); |
| 672 | + |
| 673 | + canvas.addEventListener('click', (e) => { |
| 674 | + if (!currentImageState.img) return; |
| 675 | + if (!eyedropperActive) return; |
| 676 | + const rect = canvas.getBoundingClientRect(); |
| 677 | + const scaleX = canvas.width / rect.width; |
| 678 | + const scaleY = canvas.height / rect.height; |
| 679 | + const x = Math.floor((e.clientX - rect.left) * scaleX); |
| 680 | + const y = Math.floor((e.clientY - rect.top) * scaleY); |
| 681 | + const pixel = ctx.getImageData(x, y, 1, 1).data; |
| 682 | + const hex = '#' + [pixel[0], pixel[1], pixel[2]].map(v => v.toString(16).padStart(2, '0')).join(''); |
| 683 | + bgColorPicker.value = hex; |
| 684 | + deactivateEyedropper(); |
| 685 | + }); |
| 686 | + |
| 687 | + eyedropperButton.addEventListener('click', () => { |
| 688 | + if (window.EyeDropper) { |
| 689 | + const dropper = new EyeDropper(); |
| 690 | + eyedropperButton.classList.add('picking'); |
| 691 | + dropper.open().then(result => { |
| 692 | + bgColorPicker.value = result.sRGBHex; |
| 693 | + }).catch(() => {}).finally(() => { |
| 694 | + eyedropperButton.classList.remove('picking'); |
| 695 | + }); |
| 696 | + } else { |
| 697 | + // Fallback: toggle canvas click-to-pick mode |
| 698 | + if (eyedropperActive) { |
| 699 | + deactivateEyedropper(); |
| 700 | + } else { |
| 701 | + eyedropperActive = true; |
| 702 | + eyedropperButton.classList.add('picking'); |
| 703 | + canvas.style.cursor = 'crosshair'; |
| 704 | + } |
| 705 | + } |
| 706 | + }); |
| 707 | + |
| 708 | + function deactivateEyedropper() { |
| 709 | + eyedropperActive = false; |
| 710 | + eyedropperButton.classList.remove('picking'); |
| 711 | + canvas.style.cursor = ''; |
| 712 | + } |
| 713 | + |
| 714 | + removeBgButton.addEventListener('click', removeBackground); |
| 715 | + resetImageButton.addEventListener('click', resetToOriginal); |
| 716 | + |
491 | 717 | function handleFile(file) { |
492 | 718 | if (!file) return; |
493 | 719 |
|
494 | 720 | fileNameDisplay.textContent = file.name; |
495 | 721 | contentArea.classList.remove('hidden'); |
496 | 722 | initialInstructions.classList.add('hidden'); |
| 723 | + originalImg = null; |
| 724 | + resetImageButton.classList.add('hidden'); |
497 | 725 |
|
498 | 726 | const reader = new FileReader(); |
499 | 727 | reader.onload = function (e) { |
@@ -530,6 +758,51 @@ <h3>No image selected</h3> |
530 | 758 | updateVisualVisibility(); |
531 | 759 | } |
532 | 760 |
|
| 761 | + function removeBackground() { |
| 762 | + if (!currentImageState.img) return; |
| 763 | + if (!originalImg) { |
| 764 | + originalImg = currentImageState.img; |
| 765 | + } |
| 766 | + |
| 767 | + canvas.width = originalImg.width; |
| 768 | + canvas.height = originalImg.height; |
| 769 | + ctx.clearRect(0, 0, canvas.width, canvas.height); |
| 770 | + ctx.drawImage(originalImg, 0, 0); |
| 771 | + |
| 772 | + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); |
| 773 | + const data = imageData.data; |
| 774 | + const tolerance = parseInt(toleranceSlider.value, 10); |
| 775 | + |
| 776 | + const hex = bgColorPicker.value; |
| 777 | + const tR = parseInt(hex.slice(1, 3), 16); |
| 778 | + const tG = parseInt(hex.slice(3, 5), 16); |
| 779 | + const tB = parseInt(hex.slice(5, 7), 16); |
| 780 | + |
| 781 | + for (let i = 0; i < data.length; i += 4) { |
| 782 | + const dist = Math.sqrt((data[i] - tR) ** 2 + (data[i + 1] - tG) ** 2 + (data[i + 2] - tB) ** 2); |
| 783 | + if (dist <= tolerance) { |
| 784 | + data[i + 3] = 0; |
| 785 | + } |
| 786 | + } |
| 787 | + |
| 788 | + ctx.putImageData(imageData, 0, 0); |
| 789 | + |
| 790 | + const newImg = new Image(); |
| 791 | + newImg.onload = function () { |
| 792 | + processImage(newImg); |
| 793 | + resetImageButton.classList.remove('hidden'); |
| 794 | + }; |
| 795 | + newImg.src = canvas.toDataURL('image/png'); |
| 796 | + } |
| 797 | + |
| 798 | + function resetToOriginal() { |
| 799 | + if (!originalImg) return; |
| 800 | + const img = originalImg; |
| 801 | + originalImg = null; |
| 802 | + resetImageButton.classList.add('hidden'); |
| 803 | + processImage(img); |
| 804 | + } |
| 805 | + |
533 | 806 | function checkForTransparency() { |
534 | 807 | const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); |
535 | 808 | const data = imageData.data; |
|
0 commit comments