boobalign-pro / index.html
deadhand3d's picture
make a tool where i can upload all images of woman body anatomy photos of different angles lenses and perspectives and crops and have a auto breast aligner that scales and aligns the breast scale for 3d reference
3809460 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BoobAlign Pro - 3D Breast Reference Tool</title>
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/feather-icons"></script>
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.1/fabric.min.js"></script>
<style>
.dropzone {
border: 3px dashed #9CA3AF;
transition: all 0.3s;
}
.dropzone-active {
border-color: #6366F1;
background-color: #EEF2FF;
}
#canvas-container {
position: relative;
}
.landmark {
position: absolute;
width: 12px;
height: 12px;
background: #EC4899;
border-radius: 50%;
transform: translate(-50%, -50%);
cursor: move;
z-index: 10;
}
.tools-panel {
transition: all 0.3s ease;
}
</style>
</head>
<body class="bg-gray-50 min-h-screen">
<div class="container mx-auto px-4 py-8">
<header class="mb-10 text-center">
<h1 class="text-4xl font-bold text-gray-800 mb-2">BoobAlign Pro</h1>
<p class="text-lg text-gray-600">Precision breast alignment for 3D modeling reference</p>
</header>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Upload Panel -->
<div class="lg:col-span-1 bg-white rounded-xl shadow-lg overflow-hidden">
<div class="p-6 border-b border-gray-200">
<h2 class="text-xl font-semibold text-gray-800">Upload Images</h2>
</div>
<div class="p-6">
<div id="dropzone" class="dropzone rounded-lg p-8 text-center cursor-pointer mb-6">
<i data-feather="upload-cloud" class="w-12 h-12 mx-auto text-gray-400 mb-3"></i>
<p class="text-gray-600 mb-1">Drag & drop images here</p>
<p class="text-sm text-gray-500">or click to browse</p>
<input type="file" id="file-input" class="hidden" multiple accept="image/*">
</div>
<button id="process-btn" class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-medium py-3 px-4 rounded-lg transition disabled:opacity-50" disabled>
<span class="flex items-center justify-center">
<i data-feather="cpu" class="w-5 h-5 mr-2"></i>
Process Images
</span>
</button>
</div>
</div>
<!-- Main Canvas -->
<div class="lg:col-span-2">
<div id="canvas-container" class="bg-white rounded-xl shadow-lg overflow-hidden">
<div class="p-4 border-b border-gray-200 flex justify-between items-center">
<h2 class="text-xl font-semibold text-gray-800">Alignment Canvas</h2>
<div class="flex space-x-2">
<button id="zoom-in" class="p-2 rounded hover:bg-gray-100">
<i data-feather="zoom-in"></i>
</button>
<button id="zoom-out" class="p-2 rounded hover:bg-gray-100">
<i data-feather="zoom-out"></i>
</button>
<button id="reset-view" class="p-2 rounded hover:bg-gray-100">
<i data-feather="refresh-cw"></i>
</button>
</div>
</div>
<div class="relative" style="height: 500px;">
<canvas id="main-canvas" class="absolute inset-0 w-full h-full"></canvas>
<div id="landmark-container" class="absolute inset-0 pointer-events-none"></div>
</div>
</div>
</div>
</div>
<!-- Tools Panel (hidden by default) -->
<div id="tools-panel" class="mt-8 bg-white rounded-xl shadow-lg overflow-hidden hidden">
<div class="p-6 border-b border-gray-200">
<h2 class="text-xl font-semibold text-gray-800">Alignment Tools</h2>
</div>
<div class="p-6 grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<h3 class="font-medium text-gray-700 mb-3">Landmarks</h3>
<div class="space-y-2">
<button id="add-nipple" class="w-full flex items-center justify-between px-4 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg">
<span>Add Nipple Point</span>
<i data-feather="circle" class="w-4 h-4 text-pink-500"></i>
</button>
<button id="add-underbust" class="w-full flex items-center justify-between px-4 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg">
<span>Add Underbust Point</span>
<i data-feather="circle" class="w-4 h-4 text-purple-500"></i>
</button>
<button id="add-side" class="w-full flex items-center justify-between px-4 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg">
<span>Add Side Point</span>
<i data-feather="circle" class="w-4 h-4 text-blue-500"></i>
</button>
</div>
</div>
<div>
<h3 class="font-medium text-gray-700 mb-3">Alignment</h3>
<div class="space-y-2">
<button id="auto-align" class="w-full flex items-center justify-between px-4 py-2 bg-indigo-100 hover:bg-indigo-200 rounded-lg">
<span>Auto-Align Breasts</span>
<i data-feather="move" class="w-4 h-4 text-indigo-600"></i>
</button>
<button id="symmetry-check" class="w-full flex items-center justify-between px-4 py-2 bg-indigo-100 hover:bg-indigo-200 rounded-lg">
<span>Check Symmetry</span>
<i data-feather="mirror" class="w-4 h-4 text-indigo-600"></i>
</button>
</div>
</div>
<div>
<h3 class="font-medium text-gray-700 mb-3">Export</h3>
<div class="space-y-2">
<button id="export-json" class="w-full flex items-center justify-between px-4 py-2 bg-green-100 hover:bg-green-200 rounded-lg">
<span>Export as JSON</span>
<i data-feather="download" class="w-4 h-4 text-green-600"></i>
</button>
<button id="export-image" class="w-full flex items-center justify-between px-4 py-2 bg-green-100 hover:bg-green-200 rounded-lg">
<span>Export as Image</span>
<i data-feather="image" class="w-4 h-4 text-green-600"></i>
</button>
</div>
</div>
</div>
</div>
<!-- Gallery (hidden by default) -->
<div id="gallery" class="mt-8 hidden">
<div class="bg-white rounded-xl shadow-lg overflow-hidden">
<div class="p-6 border-b border-gray-200">
<h2 class="text-xl font-semibold text-gray-800">Image Gallery</h2>
</div>
<div class="p-6">
<div id="thumbnails" class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
<!-- Thumbnails will be added here -->
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
feather.replace();
// Initialize Fabric.js canvas
const canvas = new fabric.Canvas('main-canvas', {
backgroundColor: '#f8fafc',
preserveObjectStacking: true
});
let currentImage = null;
let uploadedImages = [];
let landmarks = [];
// Dropzone functionality
const dropzone = document.getElementById('dropzone');
const fileInput = document.getElementById('file-input');
dropzone.addEventListener('click', () => fileInput.click());
dropzone.addEventListener('dragover', (e) => {
e.preventDefault();
dropzone.classList.add('dropzone-active');
});
dropzone.addEventListener('dragleave', () => {
dropzone.classList.remove('dropzone-active');
});
dropzone.addEventListener('drop', (e) => {
e.preventDefault();
dropzone.classList.remove('dropzone-active');
handleFiles(e.dataTransfer.files);
});
fileInput.addEventListener('change', () => {
if (fileInput.files.length > 0) {
handleFiles(fileInput.files);
}
});
function handleFiles(files) {
const processBtn = document.getElementById('process-btn');
for (let i = 0; i < files.length; i++) {
const file = files[i];
if (!file.type.match('image.*')) continue;
const reader = new FileReader();
reader.onload = function(e) {
uploadedImages.push({
name: file.name,
src: e.target.result
});
// Update UI
processBtn.disabled = false;
document.getElementById('gallery').classList.remove('hidden');
// Add thumbnail to gallery
const thumbnails = document.getElementById('thumbnails');
const thumbnail = document.createElement('div');
thumbnail.className = 'cursor-pointer group';
thumbnail.innerHTML = `
<div class="relative overflow-hidden rounded-lg aspect-square">
<img src="${e.target.result}" class="w-full h-full object-cover group-hover:opacity-75 transition">
<div class="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-20 transition"></div>
</div>
<p class="text-sm text-gray-600 truncate mt-1">${file.name}</p>
`;
thumbnail.addEventListener('click', () => loadImageToCanvas(e.target.result, file.name));
thumbnails.appendChild(thumbnail);
};
reader.readAsDataURL(file);
}
}
function loadImageToCanvas(src, name) {
const img = new Image();
img.src = src;
img.onload = function() {
// Clear previous image and landmarks
canvas.clear();
document.getElementById('landmark-container').innerHTML = '';
landmarks = [];
// Add new image
const fabricImg = new fabric.Image(img, {
left: canvas.width / 2,
top: canvas.height / 2,
originX: 'center',
originY: 'center',
selectable: false
});
// Scale image to fit canvas
const scale = Math.min(
(canvas.width * 0.9) / img.width,
(canvas.height * 0.9) / img.height
);
fabricImg.scale(scale);
canvas.add(fabricImg);
currentImage = fabricImg;
// Show tools panel
document.getElementById('tools-panel').classList.remove('hidden');
};
}
// Add landmark functionality
document.getElementById('add-nipple').addEventListener('click', () => addLandmark('nipple', 'bg-pink-500'));
document.getElementById('add-underbust').addEventListener('click', () => addLandmark('underbust', 'bg-purple-500'));
document.getElementById('add-side').addEventListener('click', () => addLandmark('side', 'bg-blue-500'));
function addLandmark(type, colorClass) {
if (!currentImage) return;
// Center position (temporary, user will drag it)
const container = document.getElementById('landmark-container');
const landmark = document.createElement('div');
landmark.className = `landmark ${colorClass} cursor-move`;
landmark.dataset.type = type;
landmark.style.left = '50%';
landmark.style.top = '50%';
// Make draggable
let isDragging = false;
let offsetX, offsetY;
landmark.addEventListener('mousedown', function(e) {
isDragging = true;
offsetX = e.clientX - landmark.getBoundingClientRect().left;
offsetY = e.clientY - landmark.getBoundingClientRect().top;
e.preventDefault(); // Prevent text selection
});
document.addEventListener('mousemove', function(e) {
if (!isDragging) return;
const containerRect = container.getBoundingClientRect();
let x = e.clientX - containerRect.left - offsetX;
let y = e.clientY - containerRect.top - offsetY;
// Constrain to container
x = Math.max(0, Math.min(x, containerRect.width));
y = Math.max(0, Math.min(y, containerRect.height));
landmark.style.left = x + 'px';
landmark.style.top = y + 'px';
});
document.addEventListener('mouseup', function() {
isDragging = false;
updateLandmarkPositions();
});
container.appendChild(landmark);
updateLandmarkPositions();
}
function updateLandmarkPositions() {
const container = document.getElementById('landmark-container');
const containerRect = container.getBoundingClientRect();
const canvasRect = canvas.getSelectionElement().getBoundingClientRect();
const scaleX = canvas.width / canvasRect.width;
const scaleY = canvas.height / canvasRect.height;
landmarks = [];
Array.from(container.children).forEach(landmark => {
const rect = landmark.getBoundingClientRect();
const x = (rect.left + rect.width/2 - canvasRect.left) * scaleX;
const y = (rect.top + rect.height/2 - canvasRect.top) * scaleY;
landmarks.push({
type: landmark.dataset.type,
x: x,
y: y
});
});
}
// Process button functionality
document.getElementById('process-btn').addEventListener('click', function() {
if (uploadedImages.length === 0) return;
// For demo purposes, just load the first image
loadImageToCanvas(uploadedImages[0].src, uploadedImages[0].name);
});
// Zoom controls
document.getElementById('zoom-in').addEventListener('click', function() {
if (currentImage) {
currentImage.scaleX *= 1.1;
currentImage.scaleY *= 1.1;
canvas.renderAll();
}
});
document.getElementById('zoom-out').addEventListener('click', function() {
if (currentImage) {
currentImage.scaleX *= 0.9;
currentImage.scaleY *= 0.9;
canvas.renderAll();
}
});
document.getElementById('reset-view').addEventListener('click', function() {
if (currentImage) {
const img = currentImage.getElement();
const scale = Math.min(
(canvas.width * 0.9) / img.width,
(canvas.height * 0.9) / img.height
);
currentImage.scaleX = scale;
currentImage.scaleY = scale;
currentImage.left = canvas.width / 2;
currentImage.top = canvas.height / 2;
currentImage.setCoords();
canvas.renderAll();
}
});
// Auto-align functionality (simplified for demo)
document.getElementById('auto-align').addEventListener('click', function() {
if (landmarks.length < 2) return;
// Simple demo: align nipples horizontally
const nipples = landmarks.filter(l => l.type === 'nipple');
if (nipples.length >= 2) {
const avgY = nipples.reduce((sum, l) => sum + l.y, 0) / nipples.length;
nipples.forEach(nipple => {
const element = Array.from(document.getElementById('landmark-container').children)
.find(el => el.dataset.type === 'nipple' && Math.abs(parseFloat(el.style.left) - nipple.x) < 10);
if (element) {
const containerRect = document.getElementById('landmark-container').getBoundingClientRect();
const yPos = (avgY * (containerRect.height / canvas.height)) + 'px';
element.style.top = yPos;
}
});
updateLandmarkPositions();
alert('Breasts aligned horizontally based on nipple positions!');
}
});
// Export functionality
document.getElementById('export-json').addEventListener('click', function() {
const data = {
image: currentImage ? currentImage.getSrc() : null,
landmarks: landmarks,
timestamp: new Date().toISOString()
};
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'breast-alignment-' + new Date().getTime() + '.json';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
document.getElementById('export-image').addEventListener('click', function() {
if (!currentImage) return;
// Create a temporary canvas with landmarks
const tempCanvas = document.createElement('canvas');
tempCanvas.width = canvas.width;
tempCanvas.height = canvas.height;
const ctx = tempCanvas.getContext('2d');
// Draw original image
ctx.drawImage(currentImage.getElement(), 0, 0, canvas.width, canvas.height);
// Draw landmarks
landmarks.forEach(landmark => {
ctx.beginPath();
ctx.arc(landmark.x, landmark.y, 10, 0, Math.PI * 2);
switch(landmark.type) {
case 'nipple': ctx.fillStyle = 'rgba(236, 72, 153, 0.7)'; break;
case 'underbust': ctx.fillStyle = 'rgba(168, 85, 247, 0.7)'; break;
case 'side': ctx.fillStyle = 'rgba(99, 102, 241, 0.7)'; break;
}
ctx.fill();
ctx.strokeStyle = 'white';
ctx.lineWidth = 2;
ctx.stroke();
});
// Export
const url = tempCanvas.toDataURL('image/png');
const a = document.createElement('a');
a.href = url;
a.download = 'breast-alignment-' + new Date().getTime() + '.png';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
});
});
</script>
<script>feather.replace();</script>
</body>
</html>