Spaces:
Running
Running
| <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> |