Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Smart Distance Estimation</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.1/fabric.min.js"></script> | |
<style> | |
.camera-feed { | |
position: relative; | |
width: 100%; | |
height: 0; | |
padding-bottom: 75%; /* 4:3 aspect ratio */ | |
background-color: #f0f0f0; | |
border-radius: 0.5rem; | |
overflow: hidden; | |
} | |
#canvas { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
} | |
.measurement-line { | |
stroke: #3b82f6; | |
stroke-width: 2; | |
stroke-dasharray: 5; | |
} | |
.dimension-text { | |
font-size: 12px; | |
fill: #3b82f6; | |
font-weight: bold; | |
background-color: rgba(255, 255, 255, 0.7); | |
padding: 2px 4px; | |
border-radius: 3px; | |
} | |
.object-label { | |
font-size: 10px; | |
fill: white; | |
background-color: rgba(59, 130, 246, 0.9); | |
padding: 2px 4px; | |
border-radius: 3px; | |
} | |
.depth-map { | |
position: relative; | |
width: 100%; | |
height: 0; | |
padding-bottom: 75%; | |
background-color: #1e293b; | |
border-radius: 0.5rem; | |
overflow: hidden; | |
} | |
.toggle-container { | |
position: relative; | |
display: inline-block; | |
width: 60px; | |
height: 30px; | |
} | |
.toggle-checkbox { | |
opacity: 0; | |
width: 0; | |
height: 0; | |
} | |
.toggle-slider { | |
position: absolute; | |
cursor: pointer; | |
top: 0; | |
left: 0; | |
right: 0; | |
bottom: 0; | |
background-color: #ccc; | |
transition: .4s; | |
border-radius: 34px; | |
} | |
.toggle-slider:before { | |
position: absolute; | |
content: ""; | |
height: 22px; | |
width: 22px; | |
left: 4px; | |
bottom: 4px; | |
background-color: white; | |
transition: .4s; | |
border-radius: 50%; | |
} | |
.toggle-checkbox:checked + .toggle-slider { | |
background-color: #3b82f6; | |
} | |
.toggle-checkbox:checked + .toggle-slider:before { | |
transform: translateX(30px); | |
} | |
</style> | |
</head> | |
<body class="bg-gray-100 min-h-screen"> | |
<div class="container mx-auto px-4 py-8"> | |
<header class="mb-8"> | |
<h1 class="text-3xl font-bold text-gray-800 mb-2">Smart Distance Estimation</h1> | |
<p class="text-gray-600">Measure real-world distances between objects using your smartphone's camera and LiDAR</p> | |
</header> | |
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8"> | |
<!-- Camera Feed Section --> | |
<div class="bg-white rounded-lg shadow-md p-4"> | |
<div class="flex justify-between items-center mb-4"> | |
<h2 class="text-xl font-semibold text-gray-700">Camera Feed</h2> | |
<div class="flex items-center space-x-4"> | |
<div class="flex items-center"> | |
<span class="mr-2 text-sm text-gray-600">LiDAR</span> | |
<label class="toggle-container"> | |
<input type="checkbox" id="lidarToggle" class="toggle-checkbox" checked> | |
<span class="toggle-slider"></span> | |
</label> | |
</div> | |
<button id="captureBtn" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-md text-sm"> | |
Capture Image | |
</button> | |
</div> | |
</div> | |
<div class="camera-feed"> | |
<video id="video" autoplay playsinline class="w-full h-full object-cover" style="display: none;"></video> | |
<canvas id="canvas" width="640" height="480"></canvas> | |
</div> | |
<div class="mt-4 flex justify-center"> | |
<button id="startCameraBtn" class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded-md text-sm mr-2"> | |
Start Camera | |
</button> | |
<button id="stopCameraBtn" class="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-md text-sm"> | |
Stop Camera | |
</button> | |
</div> | |
</div> | |
<!-- Depth Map Section --> | |
<div class="bg-white rounded-lg shadow-md p-4"> | |
<div class="flex justify-between items-center mb-4"> | |
<h2 class="text-xl font-semibold text-gray-700">Depth Estimation</h2> | |
<div class="flex items-center space-x-4"> | |
<div class="flex items-center"> | |
<span class="mr-2 text-sm text-gray-600">Auto-detect</span> | |
<label class="toggle-container"> | |
<input type="checkbox" id="autoDetectToggle" class="toggle-checkbox" checked> | |
<span class="toggle-slider"></span> | |
</label> | |
</div> | |
</div> | |
</div> | |
<div class="depth-map"> | |
<canvas id="depthCanvas" width="640" height="480"></canvas> | |
</div> | |
<div class="mt-4"> | |
<div class="flex justify-between mb-2"> | |
<span class="text-sm text-gray-600">Near</span> | |
<span class="text-sm text-gray-600">Far</span> | |
</div> | |
<div class="w-full bg-gray-200 rounded-full h-2.5"> | |
<div class="bg-gradient-to-r from-blue-500 via-purple-500 to-red-500 h-2.5 rounded-full" style="width: 100%"></div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Measurement Results --> | |
<div class="bg-white rounded-lg shadow-md p-6 mb-8"> | |
<h2 class="text-xl font-semibold text-gray-700 mb-4">Measurement Results</h2> | |
<div class="grid grid-cols-1 md:grid-cols-3 gap-4"> | |
<div class="bg-gray-50 p-4 rounded-lg"> | |
<h3 class="text-sm font-medium text-gray-500 mb-1">Horizontal Distance</h3> | |
<p class="text-2xl font-bold text-gray-800"> | |
<span id="horizontalDistance">0.00</span> <span class="text-sm">meters</span> | |
</p> | |
</div> | |
<div class="bg-gray-50 p-4 rounded-lg"> | |
<h3 class="text-sm font-medium text-gray-500 mb-1">Vertical Distance</h3> | |
<p class="text-2xl font-bold text-gray-800"> | |
<span id="verticalDistance">0.00</span> <span class="text-sm">meters</span> | |
</p> | |
</div> | |
<div class="bg-gray-50 p-4 rounded-lg"> | |
<h3 class="text-sm font-medium text-gray-500 mb-1">Depth Distance</h3> | |
<p class="text-2xl font-bold text-gray-800"> | |
<span id="depthDistance">0.00</span> <span class="text-sm">meters</span> | |
</p> | |
</div> | |
</div> | |
<div class="mt-6"> | |
<h3 class="text-lg font-medium text-gray-700 mb-2">Detected Objects</h3> | |
<div class="overflow-x-auto"> | |
<table class="min-w-full divide-y divide-gray-200"> | |
<thead class="bg-gray-50"> | |
<tr> | |
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Object</th> | |
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Width (m)</th> | |
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Height (m)</th> | |
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Distance (m)</th> | |
</tr> | |
</thead> | |
<tbody id="objectTableBody" class="bg-white divide-y divide-gray-200"> | |
<!-- Objects will be added here dynamically --> | |
</tbody> | |
</table> | |
</div> | |
</div> | |
</div> | |
<!-- Controls Section --> | |
<div class="bg-white rounded-lg shadow-md p-6"> | |
<h2 class="text-xl font-semibold text-gray-700 mb-4">Measurement Controls</h2> | |
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
<div> | |
<h3 class="text-lg font-medium text-gray-700 mb-3">Measurement Mode</h3> | |
<div class="space-y-3"> | |
<div class="flex items-center"> | |
<input id="singleMeasurement" name="measurementMode" type="radio" checked class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300"> | |
<label for="singleMeasurement" class="ml-2 block text-sm text-gray-700">Single Measurement</label> | |
</div> | |
<div class="flex items-center"> | |
<input id="multiMeasurement" name="measurementMode" type="radio" class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300"> | |
<label for="multiMeasurement" class="ml-2 block text-sm text-gray-700">Multi Measurement</label> | |
</div> | |
<div class="flex items-center"> | |
<input id="autoMeasurement" name="measurementMode" type="radio" class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300"> | |
<label for="autoMeasurement" class="ml-2 block text-sm text-gray-700">Auto Detect Objects</label> | |
</div> | |
</div> | |
<div class="mt-6"> | |
<h3 class="text-lg font-medium text-gray-700 mb-3">Reference Points</h3> | |
<div class="flex items-center space-x-4"> | |
<div class="flex items-center"> | |
<input id="floorCeiling" name="referencePoints" type="radio" checked class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300"> | |
<label for="floorCeiling" class="ml-2 block text-sm text-gray-700">Floor/Ceiling</label> | |
</div> | |
<div class="flex items-center"> | |
<input id="wallEdge" name="referencePoints" type="radio" class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300"> | |
<label for="wallEdge" class="ml-2 block text-sm text-gray-700">Wall Edge</label> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div> | |
<h3 class="text-lg font-medium text-gray-700 mb-3">Settings</h3> | |
<div class="space-y-4"> | |
<div> | |
<label for="unitSelect" class="block text-sm font-medium text-gray-700">Measurement Unit</label> | |
<select id="unitSelect" class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md"> | |
<option value="meters">Meters</option> | |
<option value="feet">Feet</option> | |
<option value="inches">Inches</option> | |
</select> | |
</div> | |
<div> | |
<label for="confidenceThreshold" class="block text-sm font-medium text-gray-700">Confidence Threshold</label> | |
<input type="range" id="confidenceThreshold" min="0" max="100" value="70" class="mt-1 w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"> | |
<div class="flex justify-between text-xs text-gray-500"> | |
<span>0%</span> | |
<span id="confidenceValue">70%</span> | |
<span>100%</span> | |
</div> | |
</div> | |
<div class="flex items-center"> | |
<input id="showDepthMap" type="checkbox" checked class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"> | |
<label for="showDepthMap" class="ml-2 block text-sm text-gray-700">Show Depth Map</label> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<script> | |
// DOM Elements | |
const video = document.getElementById('video'); | |
const canvas = document.getElementById('canvas'); | |
const depthCanvas = document.getElementById('depthCanvas'); | |
const startCameraBtn = document.getElementById('startCameraBtn'); | |
const stopCameraBtn = document.getElementById('stopCameraBtn'); | |
const captureBtn = document.getElementById('captureBtn'); | |
const horizontalDistanceEl = document.getElementById('horizontalDistance'); | |
const verticalDistanceEl = document.getElementById('verticalDistance'); | |
const depthDistanceEl = document.getElementById('depthDistance'); | |
const objectTableBody = document.getElementById('objectTableBody'); | |
const confidenceThreshold = document.getElementById('confidenceThreshold'); | |
const confidenceValue = document.getElementById('confidenceValue'); | |
const lidarToggle = document.getElementById('lidarToggle'); | |
const autoDetectToggle = document.getElementById('autoDetectToggle'); | |
const showDepthMap = document.getElementById('showDepthMap'); | |
// Canvas context | |
const ctx = canvas.getContext('2d'); | |
const depthCtx = depthCanvas.getContext('2d'); | |
// State variables | |
let isCameraOn = false; | |
let measurements = []; | |
let selectedPoints = []; | |
let detectedObjects = []; | |
// Event Listeners | |
startCameraBtn.addEventListener('click', startCamera); | |
stopCameraBtn.addEventListener('click', stopCamera); | |
captureBtn.addEventListener('click', captureImage); | |
canvas.addEventListener('click', handleCanvasClick); | |
confidenceThreshold.addEventListener('input', updateConfidenceValue); | |
showDepthMap.addEventListener('change', toggleDepthMap); | |
// Initialize | |
function init() { | |
// Set up initial UI state | |
stopCameraBtn.disabled = true; | |
captureBtn.disabled = true; | |
// Mock data for demonstration | |
setTimeout(() => { | |
mockDetectObjects(); | |
}, 1000); | |
} | |
// Camera functions | |
async function startCamera() { | |
try { | |
const stream = await navigator.mediaDevices.getUserMedia({ | |
video: { | |
facingMode: 'environment', | |
width: { ideal: 1280 }, | |
height: { ideal: 720 } | |
} | |
}); | |
video.srcObject = stream; | |
video.style.display = 'block'; | |
isCameraOn = true; | |
startCameraBtn.disabled = true; | |
stopCameraBtn.disabled = false; | |
captureBtn.disabled = false; | |
// Start processing frames | |
processVideo(); | |
} catch (err) { | |
console.error("Error accessing camera:", err); | |
alert("Could not access the camera. Please ensure you've granted camera permissions."); | |
} | |
} | |
function stopCamera() { | |
const stream = video.srcObject; | |
if (stream) { | |
const tracks = stream.getTracks(); | |
tracks.forEach(track => track.stop()); | |
video.srcObject = null; | |
video.style.display = 'none'; | |
} | |
isCameraOn = false; | |
startCameraBtn.disabled = false; | |
stopCameraBtn.disabled = true; | |
captureBtn.disabled = true; | |
} | |
function processVideo() { | |
if (!isCameraOn) return; | |
// Draw video frame to canvas | |
ctx.drawImage(video, 0, 0, canvas.width, canvas.height); | |
// Simulate object detection (in a real app, this would call your backend) | |
if (autoDetectToggle.checked) { | |
simulateObjectDetection(); | |
} | |
// Draw measurements | |
drawMeasurements(); | |
// Continue processing | |
requestAnimationFrame(processVideo); | |
} | |
function captureImage() { | |
// In a real app, this would send the image to your backend for processing | |
alert("Image captured! In a real implementation, this would be sent to the backend for processing."); | |
// For demo purposes, simulate detection | |
simulateObjectDetection(); | |
} | |
// Measurement functions | |
function handleCanvasClick(event) { | |
if (!isCameraOn) return; | |
const rect = canvas.getBoundingClientRect(); | |
const x = event.clientX - rect.left; | |
const y = event.clientY - rect.top; | |
selectedPoints.push({ x, y }); | |
if (selectedPoints.length === 2) { | |
// Calculate distance between points (in a real app, this would use your backend) | |
const distance = calculateDistance(selectedPoints[0], selectedPoints[1]); | |
measurements.push({ | |
points: [...selectedPoints], | |
distance: distance, | |
type: 'manual' | |
}); | |
selectedPoints = []; | |
updateMeasurementsUI(); | |
} | |
drawMeasurements(); | |
} | |
function calculateDistance(point1, point2) { | |
// Simple pixel distance (in a real app, this would convert to real-world units) | |
const dx = point2.x - point1.x; | |
const dy = point2.y - point1.y; | |
return Math.sqrt(dx * dx + dy * dy) / 100; // Simplified conversion | |
} | |
function drawMeasurements() { | |
// Clear canvas | |
ctx.clearRect(0, 0, canvas.width, canvas.height); | |
// Draw video frame | |
if (isCameraOn) { | |
ctx.drawImage(video, 0, 0, canvas.width, canvas.height); | |
} | |
// Draw selected points | |
selectedPoints.forEach(point => { | |
ctx.beginPath(); | |
ctx.arc(point.x, point.y, 5, 0, 2 * Math.PI); | |
ctx.fillStyle = '#3b82f6'; | |
ctx.fill(); | |
}); | |
// Draw measurements | |
measurements.forEach(measurement => { | |
const [point1, point2] = measurement.points; | |
// Draw line | |
ctx.beginPath(); | |
ctx.moveTo(point1.x, point1.y); | |
ctx.lineTo(point2.x, point2.y); | |
ctx.strokeStyle = '#3b82f6'; | |
ctx.lineWidth = 2; | |
ctx.setLineDash([5, 5]); | |
ctx.stroke(); | |
ctx.setLineDash([]); | |
// Draw distance text | |
const midX = (point1.x + point2.x) / 2; | |
const midY = (point1.y + point2.y) / 2; | |
ctx.font = '12px Arial'; | |
ctx.fillStyle = '#3b82f6'; | |
ctx.textAlign = 'center'; | |
ctx.fillText(`${measurement.distance.toFixed(2)} m`, midX, midY - 10); | |
}); | |
// Draw detected objects | |
detectedObjects.forEach(obj => { | |
// Draw bounding box | |
ctx.strokeStyle = '#10b981'; | |
ctx.lineWidth = 2; | |
ctx.strokeRect(obj.x, obj.y, obj.width, obj.height); | |
// Draw label | |
ctx.fillStyle = 'rgba(16, 185, 129, 0.8)'; | |
ctx.fillRect(obj.x, obj.y - 20, ctx.measureText(`${obj.label} (${obj.confidence}%)`).width + 10, 20); | |
ctx.fillStyle = 'white'; | |
ctx.font = '12px Arial'; | |
ctx.textAlign = 'left'; | |
ctx.fillText(`${obj.label} (${obj.confidence}%)`, obj.x + 5, obj.y - 5); | |
// Draw center point | |
ctx.beginPath(); | |
ctx.arc(obj.x + obj.width/2, obj.y + obj.height/2, 3, 0, 2 * Math.PI); | |
ctx.fillStyle = '#10b981'; | |
ctx.fill(); | |
}); | |
} | |
function updateMeasurementsUI() { | |
if (measurements.length > 0) { | |
const lastMeasurement = measurements[measurements.length - 1]; | |
const dx = Math.abs(lastMeasurement.points[1].x - lastMeasurement.points[0].x); | |
const dy = Math.abs(lastMeasurement.points[1].y - lastMeasurement.points[0].y); | |
// Determine if measurement is more horizontal or vertical | |
if (dx > dy) { | |
horizontalDistanceEl.textContent = lastMeasurement.distance.toFixed(2); | |
} else { | |
verticalDistanceEl.textContent = lastMeasurement.distance.toFixed(2); | |
} | |
} | |
} | |
// Object detection simulation | |
function simulateObjectDetection() { | |
// Clear previous detections | |
detectedObjects = []; | |
// Generate mock objects | |
const mockObjects = [ | |
{ label: 'Chair', confidence: 85, x: 150, y: 200, width: 80, height: 120 }, | |
{ label: 'Table', confidence: 92, x: 300, y: 180, width: 150, height: 90 }, | |
{ label: 'Window', confidence: 78, x: 100, y: 50, width: 200, height: 150 } | |
]; | |
// Filter by confidence threshold | |
const threshold = parseInt(confidenceThreshold.value) / 100; | |
detectedObjects = mockObjects.filter(obj => obj.confidence / 100 >= threshold); | |
// Update object table | |
updateObjectTable(); | |
// Simulate depth map | |
simulateDepthMap(); | |
} | |
function mockDetectObjects() { | |
// For demo when camera is off | |
simulateObjectDetection(); | |
drawMeasurements(); | |
} | |
function updateObjectTable() { | |
// Clear table | |
objectTableBody.innerHTML = ''; | |
// Add rows for each detected object | |
detectedObjects.forEach(obj => { | |
// Calculate approximate real-world dimensions (simplified) | |
const width = (obj.width / 100).toFixed(2); | |
const height = (obj.height / 100).toFixed(2); | |
const distance = (Math.sqrt(obj.x*obj.x + obj.y*obj.y) / 200).toFixed(2); | |
const row = document.createElement('tr'); | |
row.innerHTML = ` | |
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">${obj.label}</td> | |
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${width}</td> | |
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${height}</td> | |
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${distance}</td> | |
`; | |
objectTableBody.appendChild(row); | |
}); | |
} | |
// Depth map simulation | |
function simulateDepthMap() { | |
// Create gradient for depth effect | |
const gradient = depthCtx.createLinearGradient(0, 0, depthCanvas.width, depthCanvas.height); | |
gradient.addColorStop(0, '#0000ff'); // Near (blue) | |
gradient.addColorStop(0.5, '#ff00ff'); // Medium (purple) | |
gradient.addColorStop(1, '#ff0000'); // Far (red) | |
depthCtx.fillStyle = gradient; | |
depthCtx.fillRect(0, 0, depthCanvas.width, depthCanvas.height); | |
// Add noise for realism | |
const imageData = depthCtx.getImageData(0, 0, depthCanvas.width, depthCanvas.height); | |
const data = imageData.data; | |
for (let i = 0; i < data.length; i += 4) { | |
// Add some noise | |
const noise = Math.random() * 30 - 15; | |
data[i] = Math.min(255, Math.max(0, data[i] + noise)); | |
data[i + 1] = Math.min(255, Math.max(0, data[i + 1] + noise)); | |
data[i + 2] = Math.min(255, Math.max(0, data[i + 2] + noise)); | |
} | |
depthCtx.putImageData(imageData, 0, 0); | |
// Add detected objects to depth map | |
detectedObjects.forEach(obj => { | |
depthCtx.strokeStyle = 'white'; | |
depthCtx.lineWidth = 2; | |
depthCtx.strokeRect(obj.x, obj.y, obj.width, obj.height); | |
// Add depth text | |
const depthValue = (Math.sqrt(obj.x*obj.x + obj.y*obj.y) / 200).toFixed(2); | |
depthCtx.fillStyle = 'white'; | |
depthCtx.font = '10px Arial'; | |
depthCtx.textAlign = 'center'; | |
depthCtx.fillText(`${depthValue}m`, obj.x + obj.width/2, obj.y + obj.height/2); | |
}); | |
// Update depth distance display | |
if (detectedObjects.length > 0) { | |
const avgDepth = detectedObjects.reduce((sum, obj) => { | |
return sum + (Math.sqrt(obj.x*obj.x + obj.y*obj.y) / 200); | |
}, 0) / detectedObjects.length; | |
depthDistanceEl.textContent = avgDepth.toFixed(2); | |
} | |
} | |
// UI functions | |
function updateConfidenceValue() { | |
confidenceValue.textContent = `${confidenceThreshold.value}%`; | |
// If auto-detect is on, update detection | |
if (autoDetectToggle.checked && (isCameraOn || measurements.length > 0)) { | |
simulateObjectDetection(); | |
drawMeasurements(); | |
} | |
} | |
function toggleDepthMap() { | |
depthCanvas.style.display = showDepthMap.checked ? 'block' : 'none'; | |
} | |
// Initialize the app | |
init(); | |
</script> | |
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=designfailure/xydistance" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
</html> |