ammar101's picture
Deploy application code and models
0bb49b0
{% extends 'base.html' %}
{% load static %}
{% load i18n %}
{% block title %}{% trans "Virtual Body Scan - Virtual Fitting System" %}{% endblock %}
{% block content %}
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div class="text-center mb-8">
<h1 class="text-4xl font-bold text-gray-900 mb-4">{% trans "Virtual Body Scan" %}</h1>
<p id="scan-subtitle" class="text-xl text-gray-600">{% trans "Two images needed: full-body pose + face selfie" %}</p>
</div>
<!-- Gender Selector -->
<div class="flex justify-center mb-8">
<div class="inline-flex bg-gray-100 rounded-xl p-1.5 shadow-inner">
<button id="gender-men" onclick="switchGender('men')"
class="flex items-center gap-2 px-8 py-3 rounded-lg font-semibold text-sm transition-all duration-300 bg-white text-[#1B3A6B] shadow-md">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
</svg>
{% trans "Men" %}
</button>
<button id="gender-women" onclick="switchGender('women')"
class="flex items-center gap-2 px-8 py-3 rounded-lg font-semibold text-sm transition-all duration-300 text-gray-500 hover:text-gray-700">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
</svg>
{% trans "Women" %}
</button>
</div>
</div>
<!-- ===== MEN FLOW ===== -->
<div id="men-flow">
<!-- Height Input -->
<div class="max-w-md mx-auto mb-8">
<div class="bg-white rounded-2xl shadow-lg p-6 border border-[#7DB8D8]/40">
<label for="user-height" class="block text-sm font-semibold text-gray-700 mb-2">
{% trans "Your Height" %} <span class="text-red-500">*</span>
<span class="text-gray-400 font-normal">({% trans "required for accurate measurements" %})</span>
</label>
<div class="flex items-center gap-3">
<div class="relative flex-1">
<input type="number" id="user-height" min="100" max="250" step="1" placeholder="{% trans 'e.g. 175' %}"
class="w-full px-4 py-3 rounded-xl border-2 border-gray-200 focus:border-[#5B8FC9] focus:ring-2 focus:ring-[#5B8FC9]/30 outline-none transition-all text-lg font-medium text-gray-800 placeholder-gray-400"
oninput="validateHeightInput()">
<span class="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400 font-medium">cm</span>
</div>
<div id="height-status" class="hidden">
<span
class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-green-100 text-green-600">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M5 13l4 4L19 7">
</path>
</svg>
</span>
</div>
</div>
<p id="height-error" class="hidden text-red-500 text-sm mt-2">{% trans "Please enter a valid height between 100 and 250 cm." %}</p>
<p class="text-gray-400 text-xs mt-2">{% trans "Your height is used to calibrate YOLO body measurements for accuracy." %}</p>
</div>
</div>
<!-- Mode Selector Tabs -->
<div class="flex justify-center mb-8">
<div class="inline-flex bg-gray-100 rounded-xl p-1.5 shadow-inner">
<button id="tab-camera" onclick="switchMode('camera')"
class="flex items-center gap-2 px-6 py-3 rounded-lg font-semibold text-sm transition-all duration-300 bg-white text-[#1B3A6B] shadow-md">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z">
</path>
</svg>
{% trans "Camera Scan" %}
</button>
<button id="tab-upload" onclick="switchMode('upload')"
class="flex items-center gap-2 px-6 py-3 rounded-lg font-semibold text-sm transition-all duration-300 text-gray-500 hover:text-gray-700">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z">
</path>
</svg>
{% trans "Upload Images" %}
</button>
</div>
</div>
<!-- ===== CAMERA MODE ===== -->
<div id="camera-mode">
<!-- Progress Steps: 3 steps now (Body → Face → Process) -->
<div class="mb-12">
<div class="flex items-center justify-center space-x-2 md:space-x-4 flex-wrap gap-y-4">
<div id="step1" class="flex items-center">
<div
class="w-10 h-10 bg-[#1B3A6B] text-white rounded-full flex items-center justify-center font-bold">
1</div>
<span class="ml-2 font-medium text-gray-900">{% trans "Body Pose" %}</span>
</div>
<div class="w-8 md:w-16 h-1 bg-gray-300"></div>
<div id="step2" class="flex items-center opacity-50">
<div
class="w-10 h-10 bg-gray-300 text-gray-600 rounded-full flex items-center justify-center font-bold">
2</div>
<span class="ml-2 font-medium text-gray-600">{% trans "Face Selfie" %}</span>
</div>
<div class="w-8 md:w-16 h-1 bg-gray-300"></div>
<div id="step3" class="flex items-center opacity-50">
<div
class="w-10 h-10 bg-gray-300 text-gray-600 rounded-full flex items-center justify-center font-bold">
3</div>
<span class="ml-2 font-medium text-gray-600">{% trans "Processing" %}</span>
</div>
</div>
</div>
<!-- Camera Interface -->
<div class="bg-white rounded-2xl shadow-xl p-8">
<div id="camera-container" class="mb-6 relative">
<div class="relative bg-gray-900 rounded-xl overflow-hidden" style="aspect-ratio: 4/3;">
<video id="video" autoplay playsinline
class="w-full h-full object-cover transform scale-x-[-1]"></video>
<canvas id="canvas" class="hidden"></canvas>
<canvas id="overlay-canvas" class="absolute inset-0 pointer-events-none w-full h-full"></canvas>
<!-- Visual Guide – Full Body -->
<div id="visual-guide-body"
class="absolute inset-0 flex flex-col items-center justify-center pointer-events-none opacity-70 transition-all duration-300">
<div id="head-guide"
class="w-32 h-40 border-4 border-dashed border-white/70 rounded-full mb-2 transition-colors duration-300">
</div>
<div id="body-guide"
class="w-80 h-96 border-4 border-dashed border-white/70 rounded-3xl rounded-t-lg transition-colors duration-300">
</div>
</div>
<!-- Visual Guide – Face Selfie -->
<div id="visual-guide-face"
class="hidden absolute inset-0 flex flex-col items-center justify-center pointer-events-none opacity-70 transition-all duration-300">
<div id="face-guide"
class="w-64 h-80 border-4 border-dashed border-amber-400/80 rounded-full transition-colors duration-300">
</div>
<p class="text-amber-400 font-bold mt-4 text-lg bg-black/40 px-4 py-2 rounded-lg">
{% trans "Come close! Fill the circle with your face" %}
</p>
</div>
<!-- Countdown Overlay -->
<div id="countdown-overlay"
class="hidden absolute inset-0 flex items-center justify-center pointer-events-none z-40">
<div class="text-center">
<div id="countdown-number"
class="text-9xl font-black text-white drop-shadow-2xl animate-pulse"></div>
<p class="text-2xl font-bold text-white mt-4 bg-green-600/80 px-6 py-2 rounded-full">{%
trans "Hold still!" %}</p>
</div>
</div>
<!-- Capture Flash -->
<div id="capture-flash"
class="hidden absolute inset-0 bg-white pointer-events-none z-50 animate-flash"></div>
<!-- Status Panel -->
<div id="status-panel"
class="absolute top-4 left-0 right-0 flex justify-center pointer-events-none">
<div id="status-badge"
class="bg-black/60 backdrop-blur-md text-white px-6 py-2 rounded-full flex items-center gap-2 transition-all duration-300">
<div id="status-indicator" class="w-3 h-3 rounded-full bg-yellow-400 animate-pulse">
</div>
<span id="status-text" class="font-medium">{% trans "Initializing camera..." %}</span>
</div>
</div>
<!-- Auto-Capture Progress Bar -->
<div id="auto-capture-bar" class="hidden absolute bottom-4 left-4 right-4 pointer-events-none">
<div class="bg-black/50 rounded-full p-1">
<div id="capture-progress"
class="h-2 rounded-full bg-gradient-to-r from-green-400 to-emerald-500 transition-all duration-100"
style="width: 0%"></div>
</div>
<p class="text-center text-white text-sm mt-2 font-medium">{% trans "Auto-capturing when position is correct..." %}</p>
</div>
<!-- Loading spinner -->
<div id="loading"
class="hidden absolute inset-0 bg-black/70 flex items-center justify-center z-50">
<div class="text-center">
<div
class="animate-spin rounded-full h-16 w-16 border-t-4 border-b-4 border-white mx-auto mb-4">
</div>
<p class="text-white text-lg">{% trans "Analysing with YOLO + AI..." %}</p>
<p class="text-gray-300 text-sm mt-2">{% trans "Detecting pose, measuring body, detecting skin tone..." %}</p>
</div>
</div>
</div>
</div>
<!-- Instructions -->
<div id="instructions" class="bg-[#EAF3F8] rounded-xl p-6 mb-6">
<h3 class="font-bold text-lg text-[#1B3A6B] mb-3">{% trans "Step 1: Body Pose (Auto-Capture)" %}
</h3>
<ul class="space-y-2 text-[#2E5FA3]">
<li>&#10003; {% trans "Stand 2-3 metres away from the camera" %}</li>
<li>&#10003; {% trans "Align your full body with the guide outline" %}</li>
<li>&#10003; {% trans "Stand straight with arms slightly away from your body" %}</li>
<li>&#9889; <strong>{% trans "Image captures automatically when you're positioned correctly!" %}</strong></li>
</ul>
</div>
<!-- Preview Images -->
<div id="preview-container" class="hidden mb-6">
<h3 class="font-bold text-lg text-gray-900 mb-3">{% trans "Captured Images:" %}</h3>
<div class="grid grid-cols-2 gap-4">
<div id="front-preview-container" class="hidden">
<p class="text-sm text-gray-600 mb-2">{% trans "Body Pose" %} &#10003;</p>
<img id="front-preview" class="w-full rounded-lg border-2 border-green-400"
alt="{% trans 'Body pose' %}">
</div>
<div id="face-preview-container" class="hidden">
<p class="text-sm text-gray-600 mb-2">{% trans "Face Selfie" %} &#10003;</p>
<img id="face-preview" class="w-full rounded-lg border-2 border-amber-400"
alt="{% trans 'Face selfie' %}">
</div>
</div>
</div>
<!-- Error Message -->
<div id="error-message" class="hidden bg-red-50 border-2 border-red-200 rounded-xl p-4 mb-6">
<p class="text-red-800 font-medium"></p>
</div>
<!-- Action Buttons -->
<div class="flex gap-4 justify-center flex-wrap">
<button id="process-scan"
class="hidden bg-gradient-to-r from-emerald-600 to-teal-600 text-white px-8 py-4 rounded-lg font-bold text-lg hover:shadow-lg transition transform hover:scale-105">
{% trans "Process Scan" %}
</button>
<button id="retake"
class="hidden bg-gray-500 text-white px-6 py-3 rounded-lg font-medium hover:bg-gray-600 transition">
{% trans "Start Over" %}
</button>
</div>
</div>
</div><!-- /camera-mode -->
<!-- ===== UPLOAD MODE ===== -->
<div id="upload-mode" class="hidden">
<div class="bg-white rounded-2xl shadow-xl p-8">
<div class="text-center mb-6">
<h2 class="text-2xl font-bold text-gray-900 mb-2">{% trans "Upload Body Images" %}</h2>
<p class="text-gray-500">{% trans "Upload a full-body photo AND a face selfie for AI analysis" %}
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
<!-- Front / Body Image Upload -->
<div>
<label class="block text-sm font-semibold text-gray-700 mb-2">
{% trans "Body Pose" %} <span class="text-red-500">*</span>
<span class="text-gray-400 font-normal">({% trans "required – full body, front view" %})</span>
</label>
<div id="front-dropzone"
class="relative border-2 border-dashed border-[#5B8FC9] rounded-xl p-8 text-center cursor-pointer transition-all duration-300 hover:border-[#1B3A6B] hover:bg-[#EAF3F8]/50 group"
onclick="document.getElementById('front-file-input').click()"
ondragover="handleDragOver(event,'front-dropzone')"
ondragleave="handleDragLeave(event,'front-dropzone')" ondrop="handleDrop(event,'front')">
<input type="file" id="front-file-input" accept="image/*" class="hidden"
onchange="handleFileSelect(event,'front')">
<div id="front-upload-placeholder">
<div
class="w-16 h-16 mx-auto mb-4 bg-[#EAF3F8] rounded-full flex items-center justify-center group-hover:bg-[#7DB8D8]/30 transition-colors">
<svg class="w-8 h-8 text-[#5B8FC9]" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12">
</path>
</svg>
</div>
<p class="text-gray-600 font-medium">{% trans "Drop body photo here" %}</p>
<p class="text-gray-400 text-sm mt-1">{% trans "or click to browse" %}</p>
<p class="text-gray-400 text-xs mt-3">{% trans "Stand facing the camera, full body visible" %}</p>
</div>
<div id="front-upload-preview" class="hidden">
<img id="front-upload-img" class="max-h-64 mx-auto rounded-lg shadow-md"
alt="{% trans 'Body pose' %}">
<p class="text-green-600 font-medium mt-3">&#10003; {% trans "Body image ready" %}</p>
<button type="button" onclick="event.stopPropagation(); removeUpload('front')"
class="mt-2 text-sm text-red-500 hover:text-red-700 underline">{% trans "Remove" %}</button>
</div>
</div>
</div>
<!-- Face / Selfie Image Upload -->
<div>
<label class="block text-sm font-semibold text-gray-700 mb-2">
{% trans "Face Selfie" %} <span class="text-red-500">*</span>
<span class="text-gray-400 font-normal">({% trans "required – close-up face for skin tone" %})</span>
</label>
<div id="face-dropzone"
class="relative border-2 border-dashed border-amber-300 rounded-xl p-8 text-center cursor-pointer transition-all duration-300 hover:border-amber-500 hover:bg-amber-50/50 group"
onclick="document.getElementById('face-file-input').click()"
ondragover="handleDragOver(event,'face-dropzone')"
ondragleave="handleDragLeave(event,'face-dropzone')" ondrop="handleDrop(event,'face')">
<input type="file" id="face-file-input" accept="image/*" class="hidden"
onchange="handleFileSelect(event,'face')">
<div id="face-upload-placeholder">
<div
class="w-16 h-16 mx-auto mb-4 bg-amber-100 rounded-full flex items-center justify-center group-hover:bg-amber-200 transition-colors">
<svg class="w-8 h-8 text-amber-500" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12">
</path>
</svg>
</div>
<p class="text-gray-600 font-medium">{% trans "Drop face selfie here" %}</p>
<p class="text-gray-400 text-sm mt-1">{% trans "or click to browse" %}</p>
<p class="text-gray-400 text-xs mt-3">{% trans "Close-up of your face, good lighting" %}
</p>
</div>
<div id="face-upload-preview" class="hidden">
<img id="face-upload-img" class="max-h-64 mx-auto rounded-lg shadow-md"
alt="{% trans 'Face selfie' %}">
<p class="text-amber-600 font-medium mt-3">&#10003; {% trans "Face image ready" %}</p>
<button type="button" onclick="event.stopPropagation(); removeUpload('face')"
class="mt-2 text-sm text-red-500 hover:text-red-700 underline">{% trans "Remove" %}</button>
</div>
</div>
</div>
</div>
<!-- Upload Tips -->
<div class="bg-[#EAF3F8] rounded-xl p-5 mb-6">
<h3 class="font-bold text-[#1B3A6B] mb-2">{% trans "Tips for best results:" %}</h3>
<ul class="space-y-1.5 text-sm text-[#2E5FA3]">
<li>&#10003; {% trans "Body photo: full body visible from head to toe, good lighting" %}</li>
<li>&#10003; {% trans "Body photo: stand straight with arms slightly away from body" %}</li>
<li>&#10003; {% trans "Face selfie: close-up, face fills most of the frame" %}</li>
<li>&#10003; {% trans "Face selfie: natural lighting, no heavy filters" %}</li>
</ul>
</div>
<!-- Upload Error -->
<div id="upload-error" class="hidden bg-red-50 border-2 border-red-200 rounded-xl p-4 mb-6">
<p class="text-red-800 font-medium"></p>
</div>
<!-- Upload Loading -->
<div id="upload-loading" class="hidden bg-gray-900/80 rounded-xl p-8 mb-6">
<div class="text-center">
<div
class="animate-spin rounded-full h-12 w-12 border-t-4 border-b-4 border-[#5B8FC9] mx-auto mb-4">
</div>
<p class="text-white text-lg font-medium">{% trans "Analysing with YOLO + AI..." %}</p>
<p class="text-gray-300 text-sm mt-2">{% trans "Detecting pose, measuring body, detecting skin tone..." %}</p>
</div>
</div>
<!-- Upload Action Button -->
<div class="flex justify-center">
<button id="upload-process-btn" onclick="processUpload()" disabled
class="bg-gradient-to-r from-[#1B3A6B] to-[#2E5FA3] text-white px-10 py-4 rounded-xl font-bold text-lg shadow-lg transition-all duration-300 disabled:opacity-40 disabled:cursor-not-allowed hover:shadow-xl hover:scale-105 disabled:hover:scale-100 disabled:hover:shadow-lg">
{% trans "Process Scan" %}
</button>
</div>
</div>
</div><!-- /upload-mode -->
</div><!-- /men-flow -->
<!-- ===== WOMEN FLOW ===== -->
<div id="women-flow" class="hidden">
<div class="bg-white rounded-2xl shadow-xl p-8">
<div class="text-center mb-6">
<h2 class="text-2xl font-bold text-gray-900 mb-2">{% trans "Enter Your Measurements" %}</h2>
<p class="text-gray-500">{% trans "Enter your body measurements and upload a hand photo for skin tone" %}</p>
</div>
<!-- Required Measurements -->
<div class="mb-6">
<h3 class="font-bold text-lg text-[#1B3A6B] mb-4 flex items-center gap-2">
<span
class="w-6 h-6 bg-red-100 text-red-600 rounded-full flex items-center justify-center text-xs font-bold">*</span>
{% trans "Required Measurements" %}
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-semibold text-gray-700 mb-1">{% trans "Height" %} (cm) <span
class="text-red-500">*</span></label>
<input type="number" id="w-height" min="100" max="250" step="1" placeholder="{% trans 'e.g. 165' %}"
class="w-full px-4 py-3 rounded-xl border-2 border-gray-200 focus:border-[#5B8FC9] focus:ring-2 focus:ring-[#5B8FC9]/30 outline-none transition-all text-lg font-medium">
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-1">{% trans "Bust / Chest" %} (cm)
<span class="text-red-500">*</span></label>
<input type="number" id="w-chest" min="50" max="200" step="0.5" placeholder="{% trans 'e.g. 88' %}"
class="w-full px-4 py-3 rounded-xl border-2 border-gray-200 focus:border-[#5B8FC9] focus:ring-2 focus:ring-[#5B8FC9]/30 outline-none transition-all text-lg font-medium">
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-1">{% trans "Waist" %} (cm) <span
class="text-red-500">*</span></label>
<input type="number" id="w-waist" min="40" max="200" step="0.5" placeholder="{% trans 'e.g. 68' %}"
class="w-full px-4 py-3 rounded-xl border-2 border-gray-200 focus:border-[#5B8FC9] focus:ring-2 focus:ring-[#5B8FC9]/30 outline-none transition-all text-lg font-medium">
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-1">{% trans "Hip" %} (cm) <span
class="text-red-500">*</span></label>
<input type="number" id="w-hip" min="50" max="200" step="0.5" placeholder="{% trans 'e.g. 96' %}"
class="w-full px-4 py-3 rounded-xl border-2 border-gray-200 focus:border-[#5B8FC9] focus:ring-2 focus:ring-[#5B8FC9]/30 outline-none transition-all text-lg font-medium">
</div>
</div>
</div>
<!-- Optional Measurements -->
<div class="mb-6">
<h3 class="font-bold text-lg text-gray-600 mb-4 flex items-center gap-2">
<span
class="w-6 h-6 bg-gray-100 text-gray-500 rounded-full flex items-center justify-center text-xs">+</span>
{% trans "Optional Measurements" %}
<span class="text-sm font-normal text-gray-400">({% trans "better accuracy" %})</span>
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-semibold text-gray-700 mb-1">{% trans "Shoulder Width" %}
(cm)</label>
<input type="number" id="w-shoulder" min="20" max="70" step="0.5" placeholder="{% trans 'e.g. 38' %}"
class="w-full px-4 py-3 rounded-xl border-2 border-gray-200 focus:border-[#5B8FC9] focus:ring-2 focus:ring-[#5B8FC9]/30 outline-none transition-all text-lg font-medium">
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-1">{% trans "Inseam" %} (cm)</label>
<input type="number" id="w-inseam" min="40" max="110" step="0.5" placeholder="{% trans 'e.g. 76' %}"
class="w-full px-4 py-3 rounded-xl border-2 border-gray-200 focus:border-[#5B8FC9] focus:ring-2 focus:ring-[#5B8FC9]/30 outline-none transition-all text-lg font-medium">
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-1">{% trans "Arm Length" %}
(cm)</label>
<input type="number" id="w-arm" min="30" max="100" step="0.5" placeholder="{% trans 'e.g. 56' %}"
class="w-full px-4 py-3 rounded-xl border-2 border-gray-200 focus:border-[#5B8FC9] focus:ring-2 focus:ring-[#5B8FC9]/30 outline-none transition-all text-lg font-medium">
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-1">{% trans "Torso Length" %}
(cm)</label>
<input type="number" id="w-torso" min="20" max="80" step="0.5" placeholder="{% trans 'e.g. 42' %}"
class="w-full px-4 py-3 rounded-xl border-2 border-gray-200 focus:border-[#5B8FC9] focus:ring-2 focus:ring-[#5B8FC9]/30 outline-none transition-all text-lg font-medium">
</div>
</div>
</div>
<!-- Hand Image Upload -->
<div class="mb-6">
<h3 class="font-bold text-lg text-[#1B3A6B] mb-4 flex items-center gap-2">
<span
class="w-6 h-6 bg-red-100 text-red-600 rounded-full flex items-center justify-center text-xs font-bold">*</span>
{% trans "Hand Photo for Skin Tone" %}
</h3>
<div id="hand-dropzone"
class="relative border-2 border-dashed border-amber-300 rounded-xl p-8 text-center cursor-pointer transition-all duration-300 hover:border-amber-500 hover:bg-amber-50/50 group"
onclick="document.getElementById('hand-file-input').click()"
ondragover="handleDragOver(event,'hand-dropzone')"
ondragleave="handleDragLeave(event,'hand-dropzone')" ondrop="handleDropWomen(event)">
<input type="file" id="hand-file-input" accept="image/*" class="hidden"
onchange="handleHandFileSelect(event)">
<div id="hand-upload-placeholder">
<div
class="w-16 h-16 mx-auto mb-4 bg-amber-100 rounded-full flex items-center justify-center group-hover:bg-amber-200 transition-colors">
<svg class="w-8 h-8 text-amber-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12">
</path>
</svg>
</div>
<p class="text-gray-600 font-medium">{% trans "Drop hand photo here" %}</p>
<p class="text-gray-400 text-sm mt-1">{% trans "or click to browse" %}</p>
<p class="text-gray-400 text-xs mt-3">{% trans "Photo of the back of your hand, good lighting, no filters" %}</p>
</div>
<div id="hand-upload-preview" class="hidden">
<img id="hand-upload-img" class="max-h-64 mx-auto rounded-lg shadow-md" alt="Hand photo">
<p class="text-amber-600 font-medium mt-3">&#10003; {% trans "Hand image ready" %}</p>
<button type="button" onclick="event.stopPropagation(); removeHandUpload()"
class="mt-2 text-sm text-red-500 hover:text-red-700 underline">{% trans "Remove" %}</button>
</div>
</div>
</div>
<!-- Tips -->
<div class="bg-[#EAF3F8] rounded-xl p-5 mb-6">
<h3 class="font-bold text-[#1B3A6B] mb-2">{% trans "How to measure:" %}</h3>
<ul class="space-y-1.5 text-sm text-[#2E5FA3]">
<li>&#10003; {% trans "Use a soft measuring tape for best results" %}</li>
<li>&#10003; {% trans "Bust: measure around the fullest part of your chest" %}</li>
<li>&#10003; {% trans "Waist: measure around your natural waistline" %}</li>
<li>&#10003; {% trans "Hip: measure around the widest part of your hips" %}</li>
<li>&#9889; {% trans "Hand photo: back of hand, natural light, no makeup or filters" %}</li>
</ul>
</div>
<!-- Women Error -->
<div id="women-error" class="hidden bg-red-50 border-2 border-red-200 rounded-xl p-4 mb-6">
<p class="text-red-800 font-medium"></p>
</div>
<!-- Women Loading -->
<div id="women-loading" class="hidden bg-gray-900/80 rounded-xl p-8 mb-6">
<div class="text-center">
<div
class="animate-spin rounded-full h-12 w-12 border-t-4 border-b-4 border-[#5B8FC9] mx-auto mb-4">
</div>
<p class="text-white text-lg font-medium">{% trans "Analysing with AI..." %}</p>
<p class="text-gray-400 text-sm mt-2">{% trans "Detecting skin tone, recommending size..." %}</p>
</div>
</div>
<!-- Women Process Button -->
<div class="flex justify-center">
<button id="women-process-btn" onclick="processWomenScan()" disabled
class="bg-gradient-to-r from-[#1B3A6B] to-[#2E5FA3] text-white px-10 py-4 rounded-xl font-bold text-lg shadow-lg transition-all duration-300 disabled:opacity-40 disabled:cursor-not-allowed hover:shadow-xl hover:scale-105 disabled:hover:scale-100 disabled:hover:shadow-lg">
{% trans "Get My Size Recommendation" %}
</button>
</div>
</div>
</div><!-- /women-flow -->
</div>
<style>
@keyframes flash {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.animate-flash {
animation: flash 0.3s ease-out forwards;
}
@keyframes pulse-border {
0%,
100% {
border-color: rgba(34, 197, 94, 0.8);
}
50% {
border-color: rgba(34, 197, 94, 0.4);
}
}
.pulse-border-green {
animation: pulse-border 1s ease-in-out infinite;
}
</style>
<script src="{% static 'js/camera.js' %}"></script>
{% endblock %}
{% block extra_js %}
<script>
window.LANG_PREFIX = "{% if request.LANGUAGE_CODE == 'ar' %}/ar{% endif %}";
window.SCAN_I18N = {
statusInit: "{% trans 'Initializing camera...' %}",
statusAlign: "{% trans 'Align your body with the guide' %}",
statusNoPerson: "{% trans 'No person detected – step into frame' %}",
statusReady: "{% trans 'Ready to process your scan!' %}",
statusCameraDenied: "{% trans 'Camera access denied' %}",
errorCamera: "{% trans 'Camera access denied. Please allow camera access.' %}",
errorBodyImage: "{% trans 'Please capture a body pose image.' %}",
errorFaceImage: "{% trans 'Please capture a face selfie.' %}",
errorHeight: "{% trans 'Please enter a valid height (100-250 cm) before processing.' %}",
errorProcess: "{% trans 'Processing failed. Please try again.' %}",
errorNetwork: "{% trans 'Network error. Please check your connection and try again.' %}",
errorUploadBody: "{% trans 'Please upload a body pose image.' %}",
errorUploadFace: "{% trans 'Please upload a face selfie image.' %}",
errorUploadHeight: "{% trans 'Please enter a valid height (100-250 cm) before processing.' %}",
step1Title: "{% trans 'Step 1: Body Pose (Auto-Capture)' %}",
step1_1: "{% trans 'Stand 2-3 metres away from the camera' %}",
step1_2: "{% trans 'Align your full body with the guide outline' %}",
step1_3: "{% trans 'Stand straight with arms slightly away from your body' %}",
step1_4: "{% trans 'Image captures automatically when you\'re positioned correctly!' %}",
step2Title: "{% trans 'Step 2: Face Selfie (Auto-Capture)' %}",
step2_1: "{% trans 'Come close to the camera like a selfie!' %}",
step2_2: "{% trans 'Your face should fill most of the circle' %}",
step2_3: "{% trans 'Position your face in the centre of the guide' %}",
step2_4: "{% trans 'Ensure good, natural lighting on your face' %}",
step2_5: "{% trans 'Image captures automatically when you\'re close enough!' %}",
step3Title: "{% trans 'Step 3: Ready to Process' %}",
step3_1: "{% trans 'Both images captured successfully!' %}",
step3_2: "{% trans 'Click Process Scan to analyse your measurements & skin tone' %}",
step3_3: "{% trans 'You can start over if needed' %}",
subtitleMen: "{% trans 'Two images needed: full-body pose + face selfie' %}",
subtitleWomen: "{% trans 'Enter your measurements and upload a hand photo' %}",
errorWomenHeight: "{% trans 'Please enter a valid height between 100 and 250 cm.' %}",
errorWomenBust: "{% trans 'Please enter your bust/chest measurement.' %}",
errorWomenWaist: "{% trans 'Please enter your waist measurement.' %}",
errorWomenHip: "{% trans 'Please enter your hip measurement.' %}",
errorWomenHand: "{% trans 'Please upload a hand photo for skin tone detection.' %}"
};
</script>
<script>
// ===== STATE =====
let video, canvas, context, overlayCanvas, overlayContext;
let frontImageData = null; // body pose image
let faceImageData = null; // face selfie image
let currentStep = 1;
let analysisInterval = null;
let isAnalyzing = false;
let goodPoseStartTime = null;
const AUTO_CAPTURE_DELAY = 2000;
let countdownActive = false;
let countdownValue = 3;
document.addEventListener('DOMContentLoaded', function () {
video = document.getElementById('video');
canvas = document.getElementById('canvas');
context = canvas.getContext('2d');
overlayCanvas = document.getElementById('overlay-canvas');
overlayContext = overlayCanvas.getContext('2d');
window.addEventListener('resize', resizeOverlay);
initCamera();
document.getElementById('process-scan').addEventListener('click', processScan);
document.getElementById('retake').addEventListener('click', retake);
});
// ===== CAMERA INIT =====
async function initCamera() {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: { width: { ideal: 1280 }, height: { ideal: 960 }, facingMode: 'user' }
});
video.srcObject = stream;
video.onloadedmetadata = () => {
resizeOverlay();
startRealTimeAnalysis();
};
} catch (error) {
showError(window.SCAN_I18N ? window.SCAN_I18N.errorCamera : 'Camera access denied. Please allow camera access.');
updateStatus('error', window.SCAN_I18N ? window.SCAN_I18N.statusCameraDenied : 'Camera access denied');
}
}
function resizeOverlay() {
const preview = document.querySelector('#camera-container .relative');
if (!preview) return;
overlayCanvas.width = Math.max(1, Math.round(preview.clientWidth));
overlayCanvas.height = Math.max(1, Math.round(preview.clientHeight));
}
// ===== REAL-TIME ANALYSIS =====
function startRealTimeAnalysis() {
if (isAnalyzing) return;
isAnalyzing = true;
goodPoseStartTime = null;
updateStatus('warning', window.SCAN_I18N ? window.SCAN_I18N.statusAlign : 'Align your body with the guide');
document.getElementById('auto-capture-bar').classList.remove('hidden');
analysisInterval = setInterval(async () => {
if (!isAnalyzing || video.paused || video.ended || countdownActive) return;
const analysisCanvas = document.createElement('canvas');
analysisCanvas.width = 320;
analysisCanvas.height = 240;
const ctx = analysisCanvas.getContext('2d');
ctx.drawImage(video, 0, 0, 320, 240);
const imageData = analysisCanvas.toDataURL('image/jpeg', 0.6);
try {
const analysisMode = currentStep === 2 ? 'face' : 'body';
const response = await fetch('{% url "fitting_system:analyze_frame" %}', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ image: imageData, mode: analysisMode })
});
const result = await response.json();
handleAnalysisResult(result);
} catch (error) {
console.error('Analysis frame error:', error);
}
}, 200);
}
function stopAnalysis() {
isAnalyzing = false;
goodPoseStartTime = null;
if (analysisInterval) { clearInterval(analysisInterval); analysisInterval = null; }
overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
document.getElementById('auto-capture-bar').classList.add('hidden');
document.getElementById('capture-progress').style.width = '0%';
}
function handleAnalysisResult(result) {
if (!result.detected) {
updateStatus('error', window.SCAN_I18N ? window.SCAN_I18N.statusNoPerson : 'No person detected – step into frame');
clearLandmarks();
goodPoseStartTime = null;
updateCaptureProgress(0);
return;
}
updateStatus(result.status, result.message);
if (result.landmarks && result.landmarks.length > 0) {
drawLandmarks(result.landmarks, result.status);
}
if (result.status === 'good') {
if (!goodPoseStartTime) goodPoseStartTime = Date.now();
const elapsed = Date.now() - goodPoseStartTime;
const progress = Math.min(100, (elapsed / AUTO_CAPTURE_DELAY) * 100);
updateCaptureProgress(progress);
updateGuideColor('good');
if (elapsed >= AUTO_CAPTURE_DELAY && !countdownActive) startCountdown();
} else {
goodPoseStartTime = null;
updateCaptureProgress(0);
updateGuideColor(result.status);
}
}
function updateCaptureProgress(p) {
document.getElementById('capture-progress').style.width = p + '%';
}
function updateGuideColor(status) {
const headGuide = document.getElementById('head-guide');
const bodyGuide = document.getElementById('body-guide');
const faceGuide = document.getElementById('face-guide');
[headGuide, bodyGuide, faceGuide].forEach(el => {
if (el) el.classList.remove('border-green-400', 'border-yellow-400', 'border-red-400',
'pulse-border-green', 'border-white/70', 'border-amber-400/80');
});
if (status === 'good') {
headGuide && headGuide.classList.add('border-green-400', 'pulse-border-green');
bodyGuide && bodyGuide.classList.add('border-green-400', 'pulse-border-green');
faceGuide && faceGuide.classList.add('border-green-400', 'pulse-border-green');
} else if (status === 'warning') {
headGuide && headGuide.classList.add('border-yellow-400');
bodyGuide && bodyGuide.classList.add('border-yellow-400');
faceGuide && faceGuide.classList.add('border-amber-400/80');
} else {
headGuide && headGuide.classList.add('border-white/70');
bodyGuide && bodyGuide.classList.add('border-white/70');
faceGuide && faceGuide.classList.add('border-amber-400/80');
}
}
function startCountdown() {
countdownActive = true;
countdownValue = 3;
const overlay = document.getElementById('countdown-overlay');
const num = document.getElementById('countdown-number');
overlay.classList.remove('hidden');
num.textContent = countdownValue;
const interval = setInterval(() => {
countdownValue--;
if (countdownValue > 0) {
num.textContent = countdownValue;
} else {
clearInterval(interval);
overlay.classList.add('hidden');
triggerAutoCapture();
}
}, 1000);
}
function triggerAutoCapture() {
const flash = document.getElementById('capture-flash');
flash.classList.remove('hidden');
setTimeout(() => flash.classList.add('hidden'), 300);
if (currentStep === 1) captureBodyImage();
else if (currentStep === 2) captureFaceImage();
countdownActive = false;
}
// ===== CAPTURE FUNCTIONS =====
function captureImage() {
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
context.save();
context.scale(-1, 1);
context.drawImage(video, -canvas.width, 0, canvas.width, canvas.height);
context.restore();
return canvas.toDataURL('image/jpeg', 0.9);
}
function captureBodyImage() {
stopAnalysis();
frontImageData = captureImage();
document.getElementById('front-preview').src = frontImageData;
document.getElementById('preview-container').classList.remove('hidden');
document.getElementById('front-preview-container').classList.remove('hidden');
// Move to face selfie step
moveToFaceStep();
}
function moveToFaceStep() {
// Switch visual guides
document.getElementById('visual-guide-body').classList.add('hidden');
document.getElementById('visual-guide-face').classList.remove('hidden');
updateStep(2);
var i = window.SCAN_I18N || {};
document.getElementById('instructions').innerHTML = `
<h3 class="font-bold text-lg text-amber-700 mb-3">` + (i.step2Title || 'Step 2: Face Selfie (Auto-Capture)') + `</h3>
<ul class="space-y-2 text-amber-800">
<li>&#10003; <strong>` + (i.step2_1 || 'Come close to the camera like a selfie!') + `</strong></li>
<li>&#10003; ` + (i.step2_2 || 'Your face should fill most of the circle') + `</li>
<li>&#10003; ` + (i.step2_3 || 'Position your face in the centre of the guide') + `</li>
<li>&#10003; ` + (i.step2_4 || 'Ensure good, natural lighting on your face') + `</li>
<li>&#9889; <strong>` + (i.step2_5 || "Image captures automatically when you're close enough!") + `</strong></li>
</ul>
`;
setTimeout(() => startRealTimeAnalysis(), 500);
}
function captureFaceImage() {
stopAnalysis();
faceImageData = captureImage();
document.getElementById('face-preview').src = faceImageData;
document.getElementById('face-preview-container').classList.remove('hidden');
// Hide face guide
document.getElementById('visual-guide-face').classList.add('hidden');
// Show process button
document.getElementById('process-scan').classList.remove('hidden');
document.getElementById('retake').classList.remove('hidden');
updateStep(3);
var i3 = window.SCAN_I18N || {};
document.getElementById('instructions').innerHTML = `
<h3 class="font-bold text-lg text-emerald-700 mb-3">` + (i3.step3Title || 'Step 3: Ready to Process') + `</h3>
<ul class="space-y-2 text-emerald-800">
<li>&#10003; ` + (i3.step3_1 || 'Both images captured successfully!') + `</li>
<li>&#10003; ` + (i3.step3_2 || 'Click "Process Scan" to analyse your measurements & skin tone') + `</li>
<li>&#10003; ` + (i3.step3_3 || 'You can start over if needed') + `</li>
</ul>
`;
updateStatus('good', window.SCAN_I18N ? window.SCAN_I18N.statusReady : 'Ready to process your scan!');
}
// ===== PROCESS SCAN =====
function getValidatedHeight() {
const heightInput = document.getElementById('user-height');
const val = parseFloat(heightInput.value);
if (isNaN(val) || val < 100 || val > 250) {
return null;
}
return val;
}
function validateHeightInput() {
const heightInput = document.getElementById('user-height');
const val = parseFloat(heightInput.value);
const errorEl = document.getElementById('height-error');
const statusEl = document.getElementById('height-status');
if (heightInput.value === '') {
errorEl.classList.add('hidden');
statusEl.classList.add('hidden');
return;
}
if (isNaN(val) || val < 100 || val > 250) {
errorEl.classList.remove('hidden');
statusEl.classList.add('hidden');
heightInput.classList.add('border-red-400');
heightInput.classList.remove('border-green-400');
} else {
errorEl.classList.add('hidden');
statusEl.classList.remove('hidden');
heightInput.classList.remove('border-red-400');
heightInput.classList.add('border-green-400');
}
}
async function processScan() {
var i = window.SCAN_I18N || {};
if (!frontImageData) { showError(i.errorBodyImage || 'Please capture a body pose image.'); return; }
if (!faceImageData) { showError(i.errorFaceImage || 'Please capture a face selfie.'); return; }
const userHeight = getValidatedHeight();
if (!userHeight) {
showError(i.errorHeight || 'Please enter a valid height (100-250 cm) before processing.');
document.getElementById('user-height').focus();
return;
}
document.getElementById('loading').classList.remove('hidden');
document.getElementById('process-scan').disabled = true;
try {
const response = await fetch('{% url "fitting_system:process_scan" %}', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
front_image: frontImageData,
face_image: faceImageData,
user_height_cm: userHeight,
})
});
const data = await response.json();
if (data.success) {
window.location.href = `${window.LANG_PREFIX || ''}/recommendations/${data.session_id}/`;
} else {
showError(data.error || (window.SCAN_I18N && window.SCAN_I18N.errorProcess) || 'Processing failed. Please try again.');
document.getElementById('loading').classList.add('hidden');
document.getElementById('process-scan').disabled = false;
}
} catch (error) {
showError(window.SCAN_I18N && window.SCAN_I18N.errorNetwork ? window.SCAN_I18N.errorNetwork : 'Network error. Please check your connection and try again.');
document.getElementById('loading').classList.add('hidden');
document.getElementById('process-scan').disabled = false;
}
}
// ===== RETAKE =====
function retake() {
frontImageData = null;
faceImageData = null;
currentStep = 1;
goodPoseStartTime = null;
countdownActive = false;
document.getElementById('preview-container').classList.add('hidden');
document.getElementById('front-preview-container').classList.add('hidden');
document.getElementById('face-preview-container').classList.add('hidden');
document.getElementById('process-scan').classList.add('hidden');
document.getElementById('retake').classList.add('hidden');
document.getElementById('error-message').classList.add('hidden');
document.getElementById('visual-guide-body').classList.remove('hidden');
document.getElementById('visual-guide-face').classList.add('hidden');
var i1 = window.SCAN_I18N || {};
document.getElementById('instructions').innerHTML = `
<h3 class="font-bold text-lg text-[#1B3A6B] mb-3">` + (i1.step1Title || 'Step 1: Body Pose (Auto-Capture)') + `</h3>
<ul class="space-y-2 text-[#2E5FA3]">
<li>&#10003; ` + (i1.step1_1 || 'Stand 2-3 metres away from the camera') + `</li>
<li>&#10003; ` + (i1.step1_2 || 'Align your full body with the guide outline') + `</li>
<li>&#10003; ` + (i1.step1_3 || 'Stand straight with arms slightly away from your body') + `</li>
<li>&#9889; <strong>` + (i1.step1_4 || "Image captures automatically when you're positioned correctly!") + `</strong></li>
</ul>
`;
updateStep(1);
startRealTimeAnalysis();
}
// ===== STATUS + STEP HELPERS =====
function updateStatus(status, message) {
const badge = document.getElementById('status-badge');
const indicator = document.getElementById('status-indicator');
const text = document.getElementById('status-text');
text.textContent = message;
indicator.className = 'w-3 h-3 rounded-full animate-pulse transition-colors duration-300';
badge.className = 'backdrop-blur-md px-6 py-2 rounded-full flex items-center gap-2 transition-all duration-300 border-2';
if (status === 'good') {
indicator.classList.add('bg-green-400');
badge.classList.add('bg-green-900/60', 'border-green-400');
} else if (status === 'warning') {
indicator.classList.add('bg-yellow-400');
badge.classList.add('bg-yellow-900/60', 'border-yellow-400');
} else {
indicator.classList.add('bg-red-500');
badge.classList.add('bg-red-900/60', 'border-red-500');
}
}
function updateStep(step) {
currentStep = step;
const totalSteps = 3;
for (let i = 1; i <= totalSteps; i++) {
const stepEl = document.getElementById(`step${i}`);
if (!stepEl) continue;
if (i < step) {
stepEl.classList.remove('opacity-50');
stepEl.querySelector('div').classList.remove('bg-gray-300', 'text-gray-600', 'bg-[#1B3A6B]');
stepEl.querySelector('div').classList.add('bg-green-500', 'text-white');
stepEl.querySelector('span').classList.remove('text-gray-600');
stepEl.querySelector('span').classList.add('text-gray-900');
} else if (i === step) {
stepEl.classList.remove('opacity-50');
stepEl.querySelector('div').classList.remove('bg-gray-300', 'text-gray-600', 'bg-green-500');
stepEl.querySelector('div').classList.add('bg-[#1B3A6B]', 'text-white');
stepEl.querySelector('span').classList.remove('text-gray-600');
stepEl.querySelector('span').classList.add('text-gray-900');
} else {
stepEl.classList.add('opacity-50');
stepEl.querySelector('div').classList.remove('bg-[#1B3A6B]', 'bg-green-500', 'text-white');
stepEl.querySelector('div').classList.add('bg-gray-300', 'text-gray-600');
stepEl.querySelector('span').classList.remove('text-gray-900');
stepEl.querySelector('span').classList.add('text-gray-600');
}
}
}
function showError(message) {
const errorEl = document.getElementById('error-message');
errorEl.querySelector('p').textContent = message;
errorEl.classList.remove('hidden');
}
// ===== LANDMARK DRAWING =====
const POSE_CONNECTIONS = [
[11, 12], [11, 13], [13, 15], [12, 14], [14, 16],
[11, 23], [12, 24], [23, 24], [23, 25], [25, 27], [24, 26], [26, 28]
];
function drawLandmarks(landmarks, status) {
overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
let color = '#34d399';
if (status === 'warning') color = '#fbbf24';
if (status === 'error') color = '#ef4444';
overlayContext.strokeStyle = color;
overlayContext.lineWidth = 4;
overlayContext.lineCap = 'round';
overlayContext.lineJoin = 'round';
overlayContext.shadowBlur = 15;
overlayContext.shadowColor = color;
POSE_CONNECTIONS.forEach(([i, j]) => {
const lm1 = landmarks[i], lm2 = landmarks[j];
if (lm1 && lm2 && lm1.x !== undefined && lm2.x !== undefined) {
const x1 = (1 - lm1.x) * overlayCanvas.width;
const y1 = lm1.y * overlayCanvas.height;
const x2 = (1 - lm2.x) * overlayCanvas.width;
const y2 = lm2.y * overlayCanvas.height;
overlayContext.beginPath();
overlayContext.moveTo(x1, y1);
overlayContext.lineTo(x2, y2);
overlayContext.stroke();
}
});
const keyJoints = [11, 12, 13, 14, 15, 16, 23, 24, 25, 26, 27, 28, 0];
landmarks.forEach((lm, index) => {
if (!keyJoints.includes(index)) return;
const x = (1 - lm.x) * overlayCanvas.width;
const y = lm.y * overlayCanvas.height;
overlayContext.beginPath();
overlayContext.fillStyle = color;
overlayContext.arc(x, y, 6, 0, 2 * Math.PI);
overlayContext.fill();
overlayContext.beginPath();
overlayContext.fillStyle = '#FFFFFF';
overlayContext.arc(x, y, 3, 0, 2 * Math.PI);
overlayContext.fill();
});
overlayContext.shadowBlur = 0;
}
function clearLandmarks() {
overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
}
// ===== MODE SWITCH =====
let currentMode = 'camera';
function switchMode(mode) {
currentMode = mode;
const cameraMode = document.getElementById('camera-mode');
const uploadMode = document.getElementById('upload-mode');
const tabCamera = document.getElementById('tab-camera');
const tabUpload = document.getElementById('tab-upload');
if (mode === 'camera') {
cameraMode.classList.remove('hidden');
uploadMode.classList.add('hidden');
tabCamera.className = 'flex items-center gap-2 px-6 py-3 rounded-lg font-semibold text-sm transition-all duration-300 bg-white text-[#1B3A6B] shadow-md';
tabUpload.className = 'flex items-center gap-2 px-6 py-3 rounded-lg font-semibold text-sm transition-all duration-300 text-gray-500 hover:text-gray-700';
if (video && video.srcObject) video.play();
setTimeout(resizeOverlay, 0);
} else {
cameraMode.classList.add('hidden');
uploadMode.classList.remove('hidden');
tabCamera.className = 'flex items-center gap-2 px-6 py-3 rounded-lg font-semibold text-sm transition-all duration-300 text-gray-500 hover:text-gray-700';
tabUpload.className = 'flex items-center gap-2 px-6 py-3 rounded-lg font-semibold text-sm transition-all duration-300 bg-white text-[#1B3A6B] shadow-md';
if (video && video.srcObject) video.pause();
stopAnalysis();
}
}
// ===== UPLOAD MODE FUNCTIONS =====
let uploadFrontData = null;
let uploadFaceData = null;
function handleDragOver(e, dropzoneId) {
e.preventDefault(); e.stopPropagation();
document.getElementById(dropzoneId).classList.add('border-[#1B3A6B]', 'bg-[#EAF3F8]');
}
function handleDragLeave(e, dropzoneId) {
e.preventDefault(); e.stopPropagation();
document.getElementById(dropzoneId).classList.remove('border-[#1B3A6B]', 'bg-[#EAF3F8]');
}
function handleDrop(e, type) {
e.preventDefault(); e.stopPropagation();
const dropzoneId = type + '-dropzone';
document.getElementById(dropzoneId).classList.remove('border-[#1B3A6B]', 'bg-[#EAF3F8]');
const files = e.dataTransfer.files;
if (files.length > 0 && files[0].type.startsWith('image/')) readFileAsBase64(files[0], type);
}
function handleFileSelect(e, type) {
const files = e.target.files;
if (files.length > 0) readFileAsBase64(files[0], type);
}
function readFileAsBase64(file, type) {
const reader = new FileReader();
reader.onload = function (e) {
const base64Data = e.target.result;
if (type === 'front') {
uploadFrontData = base64Data;
document.getElementById('front-upload-img').src = base64Data;
document.getElementById('front-upload-placeholder').classList.add('hidden');
document.getElementById('front-upload-preview').classList.remove('hidden');
} else if (type === 'face') {
uploadFaceData = base64Data;
document.getElementById('face-upload-img').src = base64Data;
document.getElementById('face-upload-placeholder').classList.add('hidden');
document.getElementById('face-upload-preview').classList.remove('hidden');
}
updateUploadButton();
};
reader.readAsDataURL(file);
}
function removeUpload(type) {
if (type === 'front') {
uploadFrontData = null;
document.getElementById('front-upload-placeholder').classList.remove('hidden');
document.getElementById('front-upload-preview').classList.add('hidden');
document.getElementById('front-file-input').value = '';
} else if (type === 'face') {
uploadFaceData = null;
document.getElementById('face-upload-placeholder').classList.remove('hidden');
document.getElementById('face-upload-preview').classList.add('hidden');
document.getElementById('face-file-input').value = '';
}
updateUploadButton();
}
function updateUploadButton() {
// Both images required
document.getElementById('upload-process-btn').disabled = !(uploadFrontData && uploadFaceData);
}
async function processUpload() {
var i = window.SCAN_I18N || {};
if (!uploadFrontData) { showUploadError(i.errorUploadBody || 'Please upload a body pose image.'); return; }
if (!uploadFaceData) { showUploadError(i.errorUploadFace || 'Please upload a face selfie image.'); return; }
const userHeight = getValidatedHeight();
if (!userHeight) {
showUploadError(i.errorUploadHeight || i.errorHeight || 'Please enter a valid height (100-250 cm) before processing.');
document.getElementById('user-height').focus();
return;
}
document.getElementById('upload-loading').classList.remove('hidden');
document.getElementById('upload-process-btn').disabled = true;
document.getElementById('upload-error').classList.add('hidden');
try {
const response = await fetch('{% url "fitting_system:process_scan" %}', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
front_image: uploadFrontData,
face_image: uploadFaceData,
user_height_cm: userHeight,
})
});
const data = await response.json();
if (data.success) {
window.location.href = `${window.LANG_PREFIX || ''}/recommendations/${data.session_id}/`;
} else {
showUploadError(data.error || (window.SCAN_I18N && window.SCAN_I18N.errorProcess) || 'Processing failed. Please try again.');
document.getElementById('upload-loading').classList.add('hidden');
document.getElementById('upload-process-btn').disabled = false;
}
} catch (error) {
showUploadError(window.SCAN_I18N && window.SCAN_I18N.errorNetwork ? window.SCAN_I18N.errorNetwork : 'Network error. Please check your connection and try again.');
document.getElementById('upload-loading').classList.add('hidden');
document.getElementById('upload-process-btn').disabled = false;
}
}
function showUploadError(message) {
const errorEl = document.getElementById('upload-error');
errorEl.querySelector('p').textContent = message;
errorEl.classList.remove('hidden');
}
// ===== GENDER SWITCH =====
let selectedGender = 'men';
function switchGender(gender) {
selectedGender = gender;
const menFlow = document.getElementById('men-flow');
const womenFlow = document.getElementById('women-flow');
const btnMen = document.getElementById('gender-men');
const btnWomen = document.getElementById('gender-women');
const subtitle = document.getElementById('scan-subtitle');
const activeClass = 'flex items-center gap-2 px-8 py-3 rounded-lg font-semibold text-sm transition-all duration-300 bg-white text-[#1B3A6B] shadow-md';
const inactiveClass = 'flex items-center gap-2 px-8 py-3 rounded-lg font-semibold text-sm transition-all duration-300 text-gray-500 hover:text-gray-700';
if (gender === 'men') {
menFlow.classList.remove('hidden');
womenFlow.classList.add('hidden');
btnMen.className = activeClass;
btnWomen.className = inactiveClass;
subtitle.textContent = (window.SCAN_I18N && window.SCAN_I18N.subtitleMen) || 'Two images needed: full-body pose + face selfie';
setTimeout(resizeOverlay, 0);
} else {
menFlow.classList.add('hidden');
womenFlow.classList.remove('hidden');
btnMen.className = inactiveClass;
btnWomen.className = activeClass;
subtitle.textContent = (window.SCAN_I18N && window.SCAN_I18N.subtitleWomen) || 'Enter your measurements and upload a hand photo';
if (video && video.srcObject) video.pause();
stopAnalysis();
}
}
// ===== WOMEN FLOW =====
let uploadHandData = null;
function handleDropWomen(e) {
e.preventDefault(); e.stopPropagation();
document.getElementById('hand-dropzone').classList.remove('border-[#1B3A6B]', 'bg-[#EAF3F8]');
const files = e.dataTransfer.files;
if (files.length > 0 && files[0].type.startsWith('image/')) readHandFile(files[0]);
}
function handleHandFileSelect(e) {
const files = e.target.files;
if (files.length > 0) readHandFile(files[0]);
}
function readHandFile(file) {
const reader = new FileReader();
reader.onload = function (e) {
uploadHandData = e.target.result;
document.getElementById('hand-upload-img').src = uploadHandData;
document.getElementById('hand-upload-placeholder').classList.add('hidden');
document.getElementById('hand-upload-preview').classList.remove('hidden');
updateWomenButton();
};
reader.readAsDataURL(file);
}
function removeHandUpload() {
uploadHandData = null;
document.getElementById('hand-upload-placeholder').classList.remove('hidden');
document.getElementById('hand-upload-preview').classList.add('hidden');
document.getElementById('hand-file-input').value = '';
updateWomenButton();
}
function updateWomenButton() {
const height = parseFloat(document.getElementById('w-height').value);
const chest = parseFloat(document.getElementById('w-chest').value);
const waist = parseFloat(document.getElementById('w-waist').value);
const hip = parseFloat(document.getElementById('w-hip').value);
const hasRequired = !isNaN(height) && !isNaN(chest) && !isNaN(waist) && !isNaN(hip) && uploadHandData;
document.getElementById('women-process-btn').disabled = !hasRequired;
}
// Listen for input changes on women's required fields
document.addEventListener('DOMContentLoaded', function () {
['w-height', 'w-chest', 'w-waist', 'w-hip'].forEach(function (id) {
var el = document.getElementById(id);
if (el) el.addEventListener('input', updateWomenButton);
});
});
function showWomenError(message) {
const errorEl = document.getElementById('women-error');
errorEl.querySelector('p').textContent = message;
errorEl.classList.remove('hidden');
}
async function processWomenScan() {
document.getElementById('women-error').classList.add('hidden');
const height = parseFloat(document.getElementById('w-height').value);
const chest = parseFloat(document.getElementById('w-chest').value);
const waist = parseFloat(document.getElementById('w-waist').value);
const hip = parseFloat(document.getElementById('w-hip').value);
var i = window.SCAN_I18N || {};
if (isNaN(height) || height < 100 || height > 250) {
showWomenError(i.errorWomenHeight || 'Please enter a valid height between 100 and 250 cm.');
return;
}
if (isNaN(chest)) { showWomenError(i.errorWomenBust || 'Please enter your bust/chest measurement.'); return; }
if (isNaN(waist)) { showWomenError(i.errorWomenWaist || 'Please enter your waist measurement.'); return; }
if (isNaN(hip)) { showWomenError(i.errorWomenHip || 'Please enter your hip measurement.'); return; }
if (!uploadHandData) { showWomenError(i.errorWomenHand || 'Please upload a hand photo for skin tone detection.'); return; }
const measurements = { height, chest, waist, hip };
// Add optional fields if provided
const shoulder = parseFloat(document.getElementById('w-shoulder').value);
const inseam = parseFloat(document.getElementById('w-inseam').value);
const arm = parseFloat(document.getElementById('w-arm').value);
const torso = parseFloat(document.getElementById('w-torso').value);
if (!isNaN(shoulder)) measurements.shoulder_width = shoulder;
if (!isNaN(inseam)) measurements.inseam = inseam;
if (!isNaN(arm)) measurements.arm_length = arm;
if (!isNaN(torso)) measurements.torso_length = torso;
document.getElementById('women-loading').classList.remove('hidden');
document.getElementById('women-process-btn').disabled = true;
try {
const response = await fetch('{% url "fitting_system:process_scan_women" %}', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
hand_image: uploadHandData,
measurements: measurements,
})
});
const data = await response.json();
if (data.success) {
window.location.href = `${window.LANG_PREFIX || ''}/recommendations/${data.session_id}/`;
} else {
showWomenError(data.error || i.errorProcess || 'Processing failed. Please try again.');
document.getElementById('women-loading').classList.add('hidden');
document.getElementById('women-process-btn').disabled = false;
}
} catch (error) {
showWomenError(i.errorNetwork || 'Network error. Please check your connection and try again.');
document.getElementById('women-loading').classList.add('hidden');
document.getElementById('women-process-btn').disabled = false;
}
}
</script>
{% endblock %}