Skip to content

Commit 8154189

Browse files
committed
Add optional controls to attempt to remove background
1 parent 1a49c33 commit 8154189

1 file changed

Lines changed: 273 additions & 0 deletions

File tree

Tools/Centroid Center Calculator.html

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,129 @@
302302
font-size: 0.875rem;
303303
text-align: center;
304304
}
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; }
305428
</style>
306429
</head>
307430

@@ -372,6 +495,40 @@ <h2 class="controls-heading">Controls & Results</h2>
372495
</div>
373496
</div>
374497

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">&#9654;</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+
375532
<div id="results-text" class="results-section">
376533
<p class="results-note">Coordinates are relative to the top-left corner of the
377534
image.</p>
@@ -438,19 +595,30 @@ <h3>No image selected</h3>
438595

439596
const noAlphaWarning = document.getElementById('noAlphaWarning');
440597

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+
441606
let currentImageState = { img: null, centroid: null, bboxCenter: null };
607+
let originalImg = null;
442608

443609
imageUpload.addEventListener('change', (event) => handleFile(event.target.files[0]));
444610
saveButton.addEventListener('click', saveImageWithMarkers);
445611

446612
// --- Drag and Drop Logic ---
447613
const dropZone = document.getElementById('mainCard');
448614
dropZone.addEventListener('dragover', (e) => {
615+
if (!e.dataTransfer || !e.dataTransfer.types.includes('Files')) return;
449616
e.preventDefault();
450617
e.stopPropagation();
451618
dropZone.classList.add('drag-over');
452619
});
453620
dropZone.addEventListener('dragleave', (e) => {
621+
if (!e.dataTransfer || !e.dataTransfer.types.includes('Files')) return;
454622
e.preventDefault();
455623
e.stopPropagation();
456624
dropZone.classList.remove('drag-over');
@@ -488,12 +656,72 @@ <h3>No image selected</h3>
488656
bboxToggle.addEventListener('change', updateVisualVisibility);
489657
crosshairsToggle.addEventListener('change', updateVisualVisibility);
490658

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+
491717
function handleFile(file) {
492718
if (!file) return;
493719

494720
fileNameDisplay.textContent = file.name;
495721
contentArea.classList.remove('hidden');
496722
initialInstructions.classList.add('hidden');
723+
originalImg = null;
724+
resetImageButton.classList.add('hidden');
497725

498726
const reader = new FileReader();
499727
reader.onload = function (e) {
@@ -530,6 +758,51 @@ <h3>No image selected</h3>
530758
updateVisualVisibility();
531759
}
532760

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+
533806
function checkForTransparency() {
534807
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
535808
const data = imageData.data;

0 commit comments

Comments
 (0)