anycoder-58238afe / index.html
mrrobot420's picture
Upload folder using huggingface_hub
b536399 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Precision Measure & Rate Tool</title>
<!-- Import FontAwesome for Icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root {
--primary: #4f46e5;
--primary-hover: #4338ca;
--secondary: #10b981;
--danger: #ef4444;
--bg-dark: #0f172a;
--bg-card: #1e293b;
--text-light: #f8fafc;
--text-gray: #94a3b8;
--border: #334155;
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
}
body {
background-color: var(--bg-dark);
color: var(--text-light);
display: flex;
flex-direction: column;
min-height: 100vh;
}
/* --- Header --- */
header {
background-color: var(--bg-card);
border-bottom: 1px solid var(--border);
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: var(--shadow);
z-index: 10;
}
.brand {
font-size: 1.5rem;
font-weight: 700;
color: var(--primary);
display: flex;
align-items: center;
gap: 0.5rem;
}
.anycoder-link {
color: var(--text-gray);
text-decoration: none;
font-size: 0.9rem;
transition: color 0.3s;
display: flex;
align-items: center;
gap: 0.5rem;
}
.anycoder-link:hover {
color: var(--primary);
}
/* --- Main Layout --- */
main {
flex: 1;
display: flex;
flex-direction: column;
padding: 2rem;
gap: 2rem;
max-width: 1600px;
margin: 0 auto;
width: 100%;
}
/* --- Upload Section --- */
#upload-section {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 60vh;
border: 2px dashed var(--border);
border-radius: 1rem;
background-color: rgba(30, 41, 59, 0.5);
transition: all 0.3s ease;
cursor: pointer;
}
#upload-section:hover, #upload-section.drag-over {
border-color: var(--primary);
background-color: rgba(79, 70, 229, 0.1);
}
.upload-content {
text-align: center;
pointer-events: none; /* Let clicks pass to container */
}
.upload-icon {
font-size: 4rem;
color: var(--text-gray);
margin-bottom: 1rem;
}
.upload-text {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.upload-subtext {
color: var(--text-gray);
}
/* --- Workspace (Hidden initially) --- */
#workspace {
display: none;
flex-direction: row;
gap: 2rem;
height: calc(100vh - 150px);
}
/* Left Column: Canvas Area */
.canvas-container {
flex: 2;
background-color: var(--bg-card);
border-radius: 1rem;
border: 1px solid var(--border);
position: relative;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
box-shadow: var(--shadow);
}
canvas {
max-width: 100%;
max-height: 100%;
cursor: crosshair;
}
.canvas-overlay-msg {
position: absolute;
top: 1rem;
left: 50%;
transform: translateX(-50%);
background-color: rgba(0,0,0,0.7);
padding: 0.5rem 1rem;
border-radius: 2rem;
font-size: 0.9rem;
pointer-events: none;
backdrop-filter: blur(4px);
z-index: 5;
}
/* Right Column: Controls & Analysis */
.controls-sidebar {
flex: 1;
display: flex;
flex-direction: column;
gap: 1.5rem;
overflow-y: auto;
padding-right: 0.5rem;
}
.panel {
background-color: var(--bg-card);
border: 1px solid var(--border);
border-radius: 0.75rem;
padding: 1.5rem;
box-shadow: var(--shadow);
}
.panel-title {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--primary);
}
/* Tool Buttons */
.tool-group {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
margin-bottom: 1rem;
}
button {
padding: 0.75rem 1rem;
border: none;
border-radius: 0.5rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.btn-primary {
background-color: var(--primary);
color: white;
}
.btn-primary:hover { background-color: var(--primary-hover); }
.btn-secondary {
background-color: var(--secondary);
color: white;
}
.btn-secondary:hover { background-color: #059669; }
.btn-outline {
background-color: transparent;
border: 1px solid var(--border);
color: var(--text-gray);
}
.btn-outline:hover {
border-color: var(--text-light);
color: var(--text-light);
}
.btn-danger {
background-color: rgba(239, 68, 68, 0.1);
color: var(--danger);
}
.btn-danger:hover { background-color: rgba(239, 68, 68, 0.2); }
button.active {
ring: 2px solid var(--primary);
box-shadow: 0 0 0 2px var(--primary);
}
/* Inputs */
.input-group {
margin-bottom: 1rem;
}
label {
display: block;
margin-bottom: 0.5rem;
font-size: 0.9rem;
color: var(--text-gray);
}
input[type="number"], select, textarea {
width: 100%;
padding: 0.75rem;
background-color: var(--bg-dark);
border: 1px solid var(--border);
border-radius: 0.5rem;
color: var(--text-light);
font-size: 1rem;
}
input:focus, select:focus, textarea:focus {
outline: none;
border-color: var(--primary);
}
/* Rating Stars */
.rating-stars {
display: flex;
flex-direction: row-reverse;
justify-content: flex-end;
gap: 0.25rem;
}
.rating-stars input { display: none; }
.rating-stars label {
font-size: 1.5rem;
color: var(--border);
cursor: pointer;
margin: 0;
transition: color 0.2s;
}
.rating-stars input:checked ~ label,
.rating-stars label:hover,
.rating-stars label:hover ~ label {
color: #fbbf24; /* Amber-400 */
}
/* Logs & Results */
.log-list {
list-style: none;
max-height: 150px;
overflow-y: auto;
}
.log-item {
display: flex;
justify-content: space-between;
padding: 0.75rem 0;
border-bottom: 1px solid var(--border);
font-size: 0.9rem;
}
.log-item:last-child { border-bottom: none; }
.badge {
background-color: var(--primary);
color: white;
padding: 0.1rem 0.5rem;
border-radius: 1rem;
font-size: 0.75rem;
margin-left: 0.5rem;
}
/* Modal for Calibration */
.modal-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.8);
display: none;
justify-content: center;
align-items: center;
z-index: 100;
}
.modal {
background: var(--bg-card);
padding: 2rem;
border-radius: 1rem;
width: 90%;
max-width: 400px;
text-align: center;
}
/* Responsive */
@media (max-width: 900px) {
#workspace {
flex-direction: column;
height: auto;
}
.canvas-container {
min-height: 400px;
}
}
</style>
</head>
<body>
<header>
<div class="brand">
<i class="fa-solid fa-ruler-combined"></i> Measure & Rate
</div>
<a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link">
Built with anycoder <i class="fa-solid fa-arrow-up-right-from-square"></i>
</a>
</header>
<main>
<!-- Upload Section -->
<section id="upload-section">
<input type="file" id="file-input" accept="image/*" style="display: none;">
<div class="upload-content">
<i class="fa-solid fa-cloud-arrow-up upload-icon"></i>
<div class="upload-text">Click or Drag Image Here</div>
<div class="upload-subtext">Supports JPG, PNG, WEBP</div>
</div>
</section>
<!-- Workspace -->
<section id="workspace">
<!-- Canvas Area -->
<div class="canvas-container" id="canvas-wrapper">
<canvas id="main-canvas"></canvas>
<div id="status-msg" class="canvas-overlay-msg">Select a tool to begin</div>
</div>
<!-- Sidebar -->
<aside class="controls-sidebar">
<!-- Calibration Panel -->
<div class="panel">
<div class="panel-title">
<i class="fa-solid fa-scale-balanced"></i> Calibration
</div>
<p style="font-size: 0.85rem; color: var(--text-gray); margin-bottom: 1rem;">
To get accurate measurements, first define a known length (e.g., a ruler or coin).
</p>
<div class="tool-group">
<button class="btn-outline" id="btn-calibrate">
<i class="fa-solid fa-pen-ruler"></i> Set Scale
</button>
<button class="btn-outline" id="btn-measure">
<i class="fa-solid fa-maximize"></i> Measure
</button>
</div>
<div id="calibration-info" style="display: none; background: rgba(16, 185, 129, 0.1); padding: 0.75rem; border-radius: 0.5rem; border: 1px solid var(--secondary);">
<div style="color: var(--secondary); font-size: 0.9rem; margin-bottom: 0.25rem;">
<i class="fa-solid fa-check-circle"></i> Calibrated
</div>
<div style="font-size: 0.8rem; color: var(--text-gray);">
1 unit = <span id="scale-value">0</span> px
</div>
</div>
</div>
<!-- Measurement Log -->
<div class="panel">
<div class="panel-title">
<i class="fa-solid fa-list-ol"></i> Measurements
</div>
<ul class="log-list" id="measure-log">
<li class="log-item" style="color: var(--text-gray); justify-content: center;">No measurements yet</li>
</ul>
</div>
<!-- Rating & Analysis -->
<div class="panel">
<div class="panel-title">
<i class="fa-solid fa-star"></i> Detailed Analysis
</div>
<form id="rating-form">
<div class="input-group">
<label>Symmetry / Balance</label>
<div class="rating-stars">
<input type="radio" id="sym1" name="symmetry" value="5"><label for="sym1" title="Excellent"><i class="fa-solid fa-star"></i></label>
<input type="radio" id="sym2" name="symmetry" value="4"><label for="sym2" title="Good"><i class="fa-solid fa-star"></i></label>
<input type="radio" id="sym3" name="symmetry" value="3"><label for="sym3" title="Average"><i class="fa-solid fa-star"></i></label>
<input type="radio" id="sym4" name="symmetry" value="2"><label for="sym4" title="Fair"><i class="fa-solid fa-star"></i></label>
<input type="radio" id="sym5" name="symmetry" value="1"><label for="sym5" title="Poor"><i class="fa-solid fa-star"></i></label>
</div>
</div>
<div class="input-group">
<label>Clarity / Focus</label>
<div class="rating-stars">
<input type="radio" id="clr1" name="clarity" value="5"><label for="clr1"><i class="fa-solid fa-star"></i></label>
<input type="radio" id="clr2" name="clarity" value="4"><label for="clr2"><i class="fa-solid fa-star"></i></label>
<input type="radio" id="clr3" name="clarity" value="3"><label for="clr3"><i class="fa-solid fa-star"></i></label>
<input type="radio" id="clr4" name="clarity" value="2"><label for="clr4"><i class="fa-solid fa-star"></i></label>
<input type="radio" id="clr5" name="clarity" value="1"><label for="clr5"><i class="fa-solid fa-star"></i></label>
</div>
</div>
<div class="input-group">
<label>Overall Impression</label>
<textarea rows="3" placeholder="Enter detailed notes here..."></textarea>
</div>
<button type="submit" class="btn-primary" style="width: 100%;">
Save Analysis Report
</button>
</form>
</div>
<button class="btn-danger" id="btn-reset">
<i class="fa-solid fa-trash"></i> Reset Image
</button>
</aside>
</section>
</main>
<!-- Calibration Modal -->
<div class="modal-overlay" id="calibration-modal">
<div class="modal">
<h3 style="margin-bottom: 1rem;">Set Reference Length</h3>
<p style="margin-bottom: 1.5rem; color: var(--text-gray); font-size: 0.9rem;">
Enter the real-world length of the line you just drew.
</p>
<div class="input-group" style="text-align: left;">
<label>Length Value</label>
<div style="display: flex; gap: 0.5rem;">
<input type="number" id="calibration-value" step="0.1" placeholder="e.g. 1.0">
<select id="calibration-unit" style="width: 100px;">
<option value="cm">cm</option>
<option value="in">in</option>
<option value="mm">mm</option>
<option value="px">px</option>
</select>
</div>
</div>
<div style="display: flex; gap: 1rem; justify-content: center;">
<button class="btn-outline" id="btn-cancel-cal">Cancel</button>
<button class="btn-primary" id="btn-confirm-cal">Confirm Scale</button>
</div>
</div>
</div>
<!-- Notification Toast -->
<div id="toast" style="position: fixed; bottom: 2rem; right: 2rem; background: var(--bg-card); border: 1px solid var(--primary); padding: 1rem 2rem; border-radius: 0.5rem; transform: translateY(150%); transition: transform 0.3s; z-index: 200; box-shadow: var(--shadow);">
<i class="fa-solid fa-circle-info" style="color: var(--primary); margin-right: 0.5rem;"></i>
<span id="toast-msg">Notification</span>
</div>
<script>
/**
* Application Logic
* Handles Image Loading, Canvas Drawing, Math for Measurement, and UI State
*/
const App = {
state: {
mode: 'idle', // idle, calibrating, measuring
image: null,
scale: null, // pixels per unit
unit: 'cm',
points: [], // [ {x,y}, {x,y} ]
measurements: [],
isDrawing: false
},
elements: {
uploadSection: document.getElementById('upload-section'),
fileInput: document.getElementById('file-input'),
workspace: document.getElementById('workspace'),
canvas: document.getElementById('main-canvas'),
ctx: document.getElementById('main-canvas').getContext('2d'),
statusMsg: document.getElementById('status-msg'),
calBtn: document.getElementById('btn-calibrate'),
measBtn: document.getElementById('btn-measure'),
calModal: document.getElementById('calibration-modal'),
calInput: document.getElementById('calibration-value'),
calUnit: document.getElementById('calibration-unit'),
btnConfirmCal: document.getElementById('btn-confirm-cal'),
btnCancelCal: document.getElementById('btn-cancel-cal'),
calInfo: document.getElementById('calibration-info'),
scaleValue: document.getElementById('scale-value'),
logList: document.getElementById('measure-log'),
resetBtn: document.getElementById('btn-reset'),
ratingForm: document.getElementById('rating-form'),
toast: document.getElementById('toast'),
toastMsg: document.getElementById('toast-msg')
},
init() {
// Event Listeners for Upload
this.elements.uploadSection.addEventListener('click', () => this.elements.fileInput.click());
this.elements.fileInput.addEventListener('change', (e) => this.handleFile(e.target.files[0]));
// Drag and Drop
this.elements.uploadSection.addEventListener('dragover', (e) => {
e.preventDefault();
this.elements.uploadSection.classList.add('drag-over');
});
this.elements.uploadSection.addEventListener('dragleave', () => {
this.elements.uploadSection.classList.remove('drag-over');
});
this.elements.uploadSection.addEventListener('drop', (e) => {
e.preventDefault();
this.elements.uploadSection.classList.remove('drag-over');
if (e.dataTransfer.files.length) this.handleFile(e.dataTransfer.files[0]);
});
// Tool Buttons
this.elements.calBtn.addEventListener('click', () => this.setMode('calibrating'));
this.elements.measBtn.addEventListener('click', () => this.setMode('measuring'));
this.elements.resetBtn.addEventListener('click', () => window.location.reload());
// Modal
this.elements.btnConfirmCal.addEventListener('click', () => this.finalizeCalibration());
this.elements.btnCancelCal.addEventListener('click', () => {
this.elements.calModal.style.display = 'none';
this.state.points = [];
this.redraw();
this.setMode('idle');
});
// Canvas Interactions
this.elements.canvas.addEventListener('mousedown', (e) => this.handleCanvasClick(e));
this.elements.canvas.addEventListener('mousemove', (e) => this.handleCanvasMove(e));
// Form
this.elements.ratingForm.addEventListener('submit', (e) => {
e.preventDefault();
this.showToast("Analysis Report Saved Successfully!");
});
},
handleFile(file) {
if (!file || !file.type.startsWith('image/')) {
this.showToast("Please upload a valid image file.");
return;
}
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => {
this.state.image = img;
this.setupCanvas();
this.elements.uploadSection.style.display = 'none';
this.elements.workspace.style.display = 'flex';
this.showToast("Image Loaded. Please Calibrate first.");
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
},
setupCanvas() {
const { canvas, ctx } = this.elements;
const container = document.getElementById('canvas-wrapper');
// Fit canvas to container while maintaining aspect ratio
// We set the internal resolution to match the image for quality
// CSS handles the display size
canvas.width = this.state.image.width;
canvas.height = this.state.image.height;
ctx.drawImage(this.state.image, 0, 0);
},
setMode(mode) {
this.state.mode = mode;
this.state.points = [];
this.redraw();
// Reset UI buttons
this.elements.calBtn.classList.remove('active');
this.elements.measBtn.classList.remove('active');
if (mode === 'calibrating') {
this.elements.calBtn.classList.add('active');
this.elements.statusMsg.textContent = "Click two points of a known length (e.g., a ruler)";
} else if (mode === 'measuring') {
if (!this.state.scale) {
this.showToast("Please Calibrate the image first!");
this.setMode('idle');
return;
}
this.elements.measBtn.classList.add('active');
this.elements.statusMsg.textContent = "Click start and end points to measure";
} else {
this.elements.statusMsg.textContent = "Select a tool to begin";
}
},
getMousePos(evt) {
const rect = this.elements.canvas.getBoundingClientRect();
const scaleX = this.elements.canvas.width / rect.width;
const scaleY = this.elements.canvas.height / rect.height;
return {
x: (evt.clientX - rect.left) * scaleX,
y: (evt.clientY - rect.top) * scaleY
};
},
handleCanvasClick(e) {
if (this.state.mode === 'idle') return;
const pos = this.getMousePos(e);
if (this.state.points.length >= 2) {
this.state.points = [pos]; // Start new line
} else {
this.state.points.push(pos);
}
this.redraw();
if (this.state.points.length === 2) {
// Action complete
if (this.state.mode === 'calibrating') {
this.elements.calModal.style.display = 'flex';
this.elements.calInput.focus();
} else if (this.state.mode === 'measuring') {
this.calculateMeasurement();
}
}
},
handleCanvasMove(e) {
if (this.state.mode === 'idle' || this.state.points.length === 0) return;
const pos = this.getMousePos(e);
this.redraw();
// Draw preview line
const ctx = this.elements.ctx;
const start = this.state.points[0];
const color = this.state.mode === 'calibrating' ? '#10b981' : '#4f46e5';
ctx.beginPath();
ctx.moveTo(start.x, start.y);
ctx.lineTo(pos.x, pos.y);
ctx.strokeStyle = color;
ctx.lineWidth = 3;
ctx.setLineDash([5, 5]);
ctx.stroke();
ctx.setLineDash([]);
},
redraw() {
const { ctx, canvas } = this.elements;
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (this.state.image) {
ctx.drawImage(this.state.image, 0, 0);
}
// Draw existing points/lines
if (this.state.points.length > 0) {
const color = this.state.mode === 'calibrating' ? '#10b981' : '#4f46e5';
ctx.fillStyle = color;
this.state.points.forEach(p => {
ctx.beginPath();
ctx.arc(p.x, p.y, 5, 0, Math.PI * 2);
ctx.fill();
});
if (this.state.points.length === 2) {
const p1 = this.state.points[0];
const p2 = this.state.points[1];
ctx.beginPath();
ctx.moveTo(p1.x, p1.y);
ctx.lineTo(p2.x, p2.y);
ctx.strokeStyle = color;
ctx.lineWidth = 3;
ctx.stroke();
}
}
},
finalizeCalibration() {
const val = parseFloat(this.elements.calInput.value);
const unit = this.elements.calUnit.value;
if (!val || val <= 0) {
alert("Please enter a valid length.");
return;
}
const p1 = this.state.points[0];
const p2 = this.state.points[1];
const pixelDist = Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
this.state.scale = pixelDist / val;
this.state.unit = unit;
// UI Update
this.elements.calModal.style.display = 'none';
this.elements.calInfo.style.display = 'block';
this.elements.scaleValue.textContent = this.state.scale.toFixed(2);
this.elements.calBtn.innerHTML = '<i class="fa-solid fa-rotate"></i> Re-Calibrate';
this.showToast(`Calibrated: 1 ${unit} = ${this.state.scale.toFixed(1)} px`);
this.setMode('measuring');
},
calculateMeasurement() {
const p1 = this.state.points[0];
const p2 = this.state.points[1];
const pixelDist = Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
const realDist = pixelDist / this.state.scale;
// Add to log
this.addLog(realDist);
this.state.points = []; // Reset for next measure
this.redraw();
},
addLog(val) {
// Remove "No measurements" if present
if (this.state.measurements.length === 0) {
this.elements.logList.innerHTML = '';
}
this.state.measurements.push(val);
const li = document.createElement('li');
li.className = 'log-item';
li.innerHTML = `
<span>Measurement #${this.state.measurements.length}</span>
<span><strong>${val.toFixed(2)}</strong> ${this.state.unit}</span>
`;
this.elements.logList.prepend(li);
},
showToast(msg) {
this.elements.toastMsg.textContent = msg;
this.elements.toast.style.transform = 'translateY(0)';
setTimeout(() => {
this.elements.toast.style.transform = 'translateY(150%)';
}, 3000);
}
};
// Initialize
document.addEventListener('DOMContentLoaded', () => App.init());
</script>
</body>
</html>