Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>SketchMaster Pro - Image to Sketch Converter</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://unpkg.com/feather-icons"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/vanta@latest/dist/vanta.globe.min.js"></script> | |
| <style> | |
| .file-input-label:hover .upload-icon { | |
| transform: translateY(-3px); | |
| transition: all 0.2s ease; | |
| } | |
| .preview-container { | |
| min-height: 400px; | |
| background: linear-gradient(135deg, #f5f7fa 0%, #e4e8eb 100%); | |
| } | |
| .parameter-slider::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| appearance: none; | |
| width: 20px; | |
| height: 20px; | |
| border-radius: 50%; | |
| background: #4f46e5; | |
| cursor: pointer; | |
| } | |
| .dropdown:hover .dropdown-menu { | |
| display: block; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-50"> | |
| <div id="vanta-bg" class="fixed inset-0 -z-10 opacity-20"></div> | |
| <div class="container mx-auto px-4 py-8 max-w-6xl"> | |
| <!-- Header --> | |
| <header class="mb-12 text-center"> | |
| <h1 class="text-4xl md:text-5xl font-bold text-gray-800 mb-2">SketchMaster Pro</h1> | |
| <p class="text-xl text-gray-600">Transform your photos into beautiful hand-drawn sketches</p> | |
| </header> | |
| <!-- Main Content --> | |
| <div class="bg-white rounded-xl shadow-xl overflow-hidden"> | |
| <div class="grid md:grid-cols-2 gap-8 p-6"> | |
| <!-- Upload Section --> | |
| <div class="space-y-6"> | |
| <div class="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center preview-container flex items-center justify-center"> | |
| <div id="preview-content" class="text-center"> | |
| <i data-feather="upload" class="w-12 h-12 mx-auto text-gray-400 mb-4"></i> | |
| <label for="image-upload" class="file-input-label cursor-pointer"> | |
| <span class="inline-flex items-center px-6 py-3 bg-indigo-600 text-white font-medium rounded-lg hover:bg-indigo-700 transition"> | |
| <i data-feather="upload" class="upload-icon mr-2 w-5 h-5"></i> | |
| Upload Image | |
| </span> | |
| </label> | |
| <input type="file" id="image-upload" accept="image/*" class="hidden"> | |
| <p class="mt-2 text-sm text-gray-500">or drag and drop</p> | |
| </div> | |
| <img id="image-preview" class="hidden max-h-full max-w-full rounded-lg" alt="Preview"> | |
| </div> | |
| <div class="space-y-4"> | |
| <div> | |
| <h3 class="font-medium text-gray-700 mb-2">Edge Detection</h3> | |
| <div class="grid grid-cols-3 gap-2"> | |
| <button class="edge-method-btn active bg-indigo-100 text-indigo-800 border border-indigo-200 px-3 py-2 rounded-lg text-sm font-medium">Canny</button> | |
| <button class="edge-method-btn bg-gray-100 text-gray-800 border border-gray-200 px-3 py-2 rounded-lg text-sm font-medium">Sobel</button> | |
| <button class="edge-method-btn bg-gray-100 text-gray-800 border border-gray-200 px-3 py-2 rounded-lg text-sm font-medium">Laplace</button> | |
| </div> | |
| </div> | |
| <div class="grid grid-cols-2 gap-4"> | |
| <div> | |
| <label for="blur-slider" class="block text-sm font-medium text-gray-700 mb-1">Blur Amount</label> | |
| <input type="range" id="blur-slider" min="0" max="5" step="0.1" value="1.2" class="w-full parameter-slider"> | |
| <div class="flex justify-between text-xs text-gray-500"> | |
| <span>0</span> | |
| <span>5</span> | |
| </div> | |
| </div> | |
| <div> | |
| <label for="thickness-slider" class="block text-sm font-medium text-gray-700 mb-1">Line Thickness</label> | |
| <input type="range" id="thickness-slider" min="1" max="10" step="1" value="2" class="w-full parameter-slider"> | |
| <div class="flex justify-between text-xs text-gray-500"> | |
| <span>1</span> | |
| <span>10</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Parameters Section --> | |
| <div class="space-y-6"> | |
| <div class="bg-gray-50 p-4 rounded-lg"> | |
| <h3 class="font-medium text-gray-700 mb-3">Threshold Options</h3> | |
| <div class="grid grid-cols-2 gap-4"> | |
| <div class="relative"> | |
| <button id="threshold-btn" class="w-full flex justify-between items-center px-4 py-2 bg-white border border-gray-300 rounded-lg shadow-sm text-left"> | |
| <span>Otsu</span> | |
| <i data-feather="chevron-down" class="w-4 h-4"></i> | |
| </button> | |
| <div id="threshold-menu" class="hidden absolute z-10 mt-1 w-full bg-white border border-gray-200 rounded-lg shadow-lg"> | |
| <a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Otsu</a> | |
| <a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Fixed</a> | |
| <a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Adaptive</a> | |
| <a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Posterize</a> | |
| </div> | |
| </div> | |
| <div id="threshold-params" class="space-y-2"> | |
| <div class="threshold-option" data-method="fixed"> | |
| <label class="block text-xs text-gray-500">Threshold Value</label> | |
| <input type="range" min="0" max="255" value="128" class="w-full parameter-slider"> | |
| </div> | |
| <div class="threshold-option hidden" data-method="adaptive"> | |
| <label class="block text-xs text-gray-500">Block Size</label> | |
| <input type="range" min="3" max="51" step="2" value="25" class="w-full parameter-slider"> | |
| </div> | |
| <div class="threshold-option hidden" data-method="posterize"> | |
| <label class="block text-xs text-gray-500">Levels</label> | |
| <div class="flex space-x-2"> | |
| <button class="posterize-level active px-3 py-1 bg-indigo-100 text-indigo-800 rounded-full text-xs">2</button> | |
| <button class="posterize-level px-3 py-1 bg-gray-100 text-gray-800 rounded-full text-xs">3</button> | |
| <button class="posterize-level px-3 py-1 bg-gray-100 text-gray-800 rounded-full text-xs">4</button> | |
| <button class="posterize-level px-3 py-1 bg-gray-100 text-gray-800 rounded-full text-xs">5</button> | |
| <button class="posterize-level px-3 py-1 bg-gray-100 text-gray-800 rounded-full text-xs">6</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="bg-gray-50 p-4 rounded-lg"> | |
| <div class="flex items-center justify-between mb-3"> | |
| <h3 class="font-medium text-gray-700">Hatching Effect</h3> | |
| <label class="relative inline-flex items-center cursor-pointer"> | |
| <input type="checkbox" id="hatch-toggle" class="sr-only peer"> | |
| <div class="w-11 h-6 bg-gray-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-indigo-600"></div> | |
| </label> | |
| </div> | |
| <div id="hatch-params" class="space-y-3 hidden"> | |
| <div> | |
| <label class="block text-xs text-gray-500">Angle</label> | |
| <input type="range" min="0" max="180" value="45" class="w-full parameter-slider"> | |
| </div> | |
| <div> | |
| <label class="block text-xs text-gray-500">Spacing</label> | |
| <input type="range" min="2" max="20" value="8" class="w-full parameter-slider"> | |
| </div> | |
| <div> | |
| <label class="block text-xs text-gray-500">Thickness</label> | |
| <input type="range" min="1" max="5" value="1" class="w-full parameter-slider"> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="flex items-center space-x-4"> | |
| <div class="flex items-center"> | |
| <input id="invert-toggle" type="checkbox" class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"> | |
| <label for="invert-toggle" class="ml-2 block text-sm text-gray-700">Invert Colors</label> | |
| </div> | |
| <div class="flex items-center"> | |
| <input id="jitter-toggle" type="checkbox" class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded" checked> | |
| <label for="jitter-toggle" class="ml-2 block text-sm text-gray-700">Enable Jitter</label> | |
| </div> | |
| </div> | |
| <button id="process-btn" class="w-full py-3 px-4 bg-indigo-600 hover:bg-indigo-700 text-white font-medium rounded-lg transition flex items-center justify-center"> | |
| <i data-feather="zap" class="w-5 h-5 mr-2"></i> | |
| Generate Sketch | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Results Section --> | |
| <div id="results-section" class="hidden border-t border-gray-200 p-6"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h2 class="text-xl font-bold text-gray-800">Your Sketch Results</h2> | |
| <button id="download-btn" class="flex items-center text-indigo-600 hover:text-indigo-800"> | |
| <i data-feather="download" class="w-4 h-4 mr-1"></i> | |
| <span>Download</span> | |
| </button> | |
| </div> | |
| <div class="grid md:grid-cols-2 gap-6"> | |
| <div class="border rounded-lg overflow-hidden"> | |
| <h3 class="bg-gray-50 px-4 py-2 text-sm font-medium text-gray-700">Original</h3> | |
| <div class="p-4"> | |
| <img id="original-display" src="" alt="Original Image" class="max-h-80 mx-auto"> | |
| </div> | |
| </div> | |
| <div class="border rounded-lg overflow-hidden"> | |
| <h3 class="bg-gray-50 px-4 py-2 text-sm font-medium text-gray-700">Sketch</h3> | |
| <div class="p-4"> | |
| <img id="result-display" src="" alt="Sketch Result" class="max-h-80 mx-auto"> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Footer --> | |
| <footer class="mt-12 text-center text-gray-500 text-sm"> | |
| <p>SketchMaster Pro - Transform your images into artistic sketches with ease</p> | |
| </footer> | |
| </div> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/opencv.js/4.5.5/opencv.js" onload="onOpenCvReady();" async></script> | |
| <script> | |
| function onOpenCvReady() { | |
| console.log('OpenCV.js is ready'); | |
| } | |
| // Initialize Vanta.js background | |
| VANTA.GLOBE({ | |
| el: "#vanta-bg", | |
| mouseControls: true, | |
| touchControls: true, | |
| gyroControls: false, | |
| minHeight: 200.00, | |
| minWidth: 200.00, | |
| scale: 1.00, | |
| scaleMobile: 1.00, | |
| color: 0x4f46e5, | |
| backgroundColor: 0xf8fafc, | |
| size: 0.7 | |
| }); | |
| // Initialize Feather Icons | |
| document.addEventListener('DOMContentLoaded', function() { | |
| feather.replace(); | |
| // File upload handling | |
| const fileInput = document.getElementById('image-upload'); | |
| const previewContent = document.getElementById('preview-content'); | |
| const imagePreview = document.getElementById('image-preview'); | |
| const previewContainer = document.querySelector('.preview-container'); | |
| fileInput.addEventListener('change', function(e) { | |
| if (e.target.files.length) { | |
| const file = e.target.files[0]; | |
| const reader = new FileReader(); | |
| reader.onload = function(event) { | |
| imagePreview.src = event.target.result; | |
| imagePreview.classList.remove('hidden'); | |
| previewContent.classList.add('hidden'); | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| }); | |
| // Drag and drop handling | |
| previewContainer.addEventListener('dragover', function(e) { | |
| e.preventDefault(); | |
| this.classList.add('border-indigo-500', 'bg-indigo-50'); | |
| }); | |
| previewContainer.addEventListener('dragleave', function() { | |
| this.classList.remove('border-indigo-500', 'bg-indigo-50'); | |
| }); | |
| previewContainer.addEventListener('drop', function(e) { | |
| e.preventDefault(); | |
| this.classList.remove('border-indigo-500', 'bg-indigo-50'); | |
| if (e.dataTransfer.files.length) { | |
| fileInput.files = e.dataTransfer.files; | |
| const event = new Event('change'); | |
| fileInput.dispatchEvent(event); | |
| } | |
| }); | |
| // Threshold method dropdown | |
| const thresholdBtn = document.getElementById('threshold-btn'); | |
| const thresholdMenu = document.getElementById('threshold-menu'); | |
| thresholdBtn.addEventListener('click', function() { | |
| thresholdMenu.classList.toggle('hidden'); | |
| }); | |
| // Close dropdown when clicking outside | |
| document.addEventListener('click', function(e) { | |
| if (!thresholdBtn.contains(e.target) && !thresholdMenu.contains(e.target)) { | |
| thresholdMenu.classList.add('hidden'); | |
| } | |
| }); | |
| // Threshold method selection | |
| const thresholdOptions = thresholdMenu.querySelectorAll('a'); | |
| thresholdOptions.forEach(option => { | |
| option.addEventListener('click', function(e) { | |
| e.preventDefault(); | |
| const method = this.textContent.toLowerCase(); | |
| thresholdBtn.querySelector('span').textContent = this.textContent; | |
| thresholdMenu.classList.add('hidden'); | |
| // Show/hide relevant parameters | |
| document.querySelectorAll('.threshold-option').forEach(el => { | |
| el.classList.add('hidden'); | |
| }); | |
| const activeOption = document.querySelector(`.threshold-option[data-method="${method}"]`); | |
| if (activeOption) activeOption.classList.remove('hidden'); | |
| }); | |
| }); | |
| // Hatching toggle | |
| const hatchToggle = document.getElementById('hatch-toggle'); | |
| const hatchParams = document.getElementById('hatch-params'); | |
| hatchToggle.addEventListener('change', function() { | |
| if (this.checked) { | |
| hatchParams.classList.remove('hidden'); | |
| } else { | |
| hatchParams.classList.add('hidden'); | |
| } | |
| }); | |
| // Edge method buttons | |
| const edgeMethodBtns = document.querySelectorAll('.edge-method-btn'); | |
| edgeMethodBtns.forEach(btn => { | |
| btn.addEventListener('click', function() { | |
| edgeMethodBtns.forEach(b => { | |
| b.classList.remove('active', 'bg-indigo-100', 'text-indigo-800', 'border-indigo-200'); | |
| b.classList.add('bg-gray-100', 'text-gray-800', 'border-gray-200'); | |
| }); | |
| this.classList.add('active', 'bg-indigo-100', 'text-indigo-800', 'border-indigo-200'); | |
| this.classList.remove('bg-gray-100', 'text-gray-800', 'border-gray-200'); | |
| }); | |
| }); | |
| // Posterize level buttons | |
| const posterizeLevels = document.querySelectorAll('.posterize-level'); | |
| posterizeLevels.forEach(btn => { | |
| btn.addEventListener('click', function() { | |
| posterizeLevels.forEach(b => { | |
| b.classList.remove('active', 'bg-indigo-100', 'text-indigo-800'); | |
| b.classList.add('bg-gray-100', 'text-gray-800'); | |
| }); | |
| this.classList.add('active', 'bg-indigo-100', 'text-indigo-800'); | |
| }); | |
| }); | |
| // Process button (simulate processing) | |
| const processBtn = document.getElementById('process-btn'); | |
| const resultsSection = document.getElementById('results-section'); | |
| const originalDisplay = document.getElementById('original-display'); | |
| const resultDisplay = document.getElementById('result-display'); | |
| processBtn.addEventListener('click', function() { | |
| if (!imagePreview.src) { | |
| alert('Please upload an image first'); | |
| return; | |
| } | |
| // Show loading state | |
| this.innerHTML = '<i data-feather="loader" class="w-5 h-5 mr-2 animate-spin"></i> Processing...'; | |
| feather.replace(); | |
| // Simulate processing delay | |
| setTimeout(() => { | |
| // Process image to sketch effect | |
| const canvas = document.createElement('canvas'); | |
| const ctx = canvas.getContext('2d'); | |
| const img = new Image(); | |
| img.onload = function() { | |
| canvas.width = img.width; | |
| canvas.height = img.height; | |
| // Apply sketch effect | |
| ctx.drawImage(img, 0, 0); | |
| // Convert to grayscale | |
| const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); | |
| const data = imageData.data; | |
| for (let i = 0; i < data.length; i += 4) { | |
| const avg = (data[i] + data[i + 1] + data[i + 2]) / 3; | |
| data[i] = avg; | |
| data[i + 1] = avg; | |
| data[i + 2] = avg; | |
| } | |
| // Convert to grayscale and invert | |
| for (let i = 0; i < data.length; i += 4) { | |
| const avg = (data[i] + data[i + 1] + data[i + 2]) / 3; | |
| data[i] = 255 - avg; | |
| data[i + 1] = 255 - avg; | |
| data[i + 2] = 255 - avg; | |
| } | |
| // Apply blur effect based on slider | |
| const blurAmount = document.getElementById('blur-slider').value; | |
| if (blurAmount > 0) { | |
| ctx.filter = `blur(${blurAmount}px)`; | |
| ctx.drawImage(canvas, 0, 0); | |
| ctx.filter = 'none'; | |
| } | |
| ctx.putImageData(imageData, 0, 0); | |
| // Show results | |
| originalDisplay.src = imagePreview.src; | |
| resultDisplay.src = canvas.toDataURL('image/png'); | |
| resultsSection.classList.remove('hidden'); | |
| }; | |
| img.src = imagePreview.src; | |
| // Reset button | |
| this.innerHTML = '<i data-feather="zap" class="w-5 h-5 mr-2"></i> Generate Sketch'; | |
| feather.replace(); | |
| // Scroll to results | |
| resultsSection.scrollIntoView({ behavior: 'smooth' }); | |
| }, 1500); | |
| }); | |
| // Download button | |
| const downloadBtn = document.getElementById('download-btn'); | |
| downloadBtn.addEventListener('click', function() { | |
| if (!resultDisplay.src) return; | |
| // Create download link | |
| const link = document.createElement('a'); | |
| link.download = 'sketchmaster-result.png'; | |
| link.href = resultDisplay.src; | |
| document.body.appendChild(link); | |
| link.click(); | |
| document.body.removeChild(link); | |
| }); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |