Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | |
| <title>Image Warp Tool</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| #image-container { | |
| touch-action: none; | |
| position: relative; | |
| overflow: hidden; | |
| background-color: #f0f0f0; | |
| } | |
| #canvas { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| cursor: move; | |
| } | |
| .modal { | |
| transition: opacity 0.3s ease; | |
| } | |
| .control-point { | |
| position: absolute; | |
| width: 24px; | |
| height: 24px; | |
| background-color: rgba(59, 130, 246, 0.8); | |
| border: 2px solid white; | |
| border-radius: 50%; | |
| transform: translate(-50%, -50%); | |
| cursor: move; | |
| z-index: 10; | |
| } | |
| .control-point:hover { | |
| background-color: rgba(29, 78, 216, 0.8); | |
| } | |
| .control-point.active { | |
| background-color: rgba(239, 68, 68, 0.8); | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-100 min-h-screen flex flex-col items-center justify-center p-4"> | |
| <div class="w-full max-w-4xl bg-white rounded-xl shadow-xl overflow-hidden"> | |
| <div class="p-6 bg-purple-600 text-white"> | |
| <h1 class="text-2xl font-bold text-center">Image Warp Tool</h1> | |
| <p class="text-center text-purple-100 mt-2">Drag points to warp specific areas of your image</p> | |
| </div> | |
| <div class="p-6"> | |
| <div class="flex flex-col items-center space-y-4"> | |
| <div class="w-full"> | |
| <label class="block text-gray-700 font-medium mb-2" for="image-upload"> | |
| Choose an image to warp | |
| </label> | |
| <div class="flex items-center space-x-2"> | |
| <input type="file" id="image-upload" accept="image/*" class="hidden"> | |
| <button id="upload-btn" class="flex-1 bg-purple-500 hover:bg-purple-600 text-white py-2 px-4 rounded-lg transition duration-200 flex items-center justify-center"> | |
| <i class="fas fa-image mr-2"></i> | |
| Select Image | |
| </button> | |
| <button id="add-point-btn" class="bg-gray-200 hover:bg-gray-300 text-gray-800 py-2 px-4 rounded-lg transition duration-200 flex items-center justify-center" disabled> | |
| <i class="fas fa-plus-circle mr-2"></i> | |
| Add Point | |
| </button> | |
| <button id="help-btn" class="bg-gray-200 hover:bg-gray-300 text-gray-800 py-2 px-4 rounded-lg transition duration-200 flex items-center justify-center"> | |
| <i class="fas fa-question-circle mr-2"></i> | |
| Help | |
| </button> | |
| </div> | |
| </div> | |
| <div id="image-container" class="w-full rounded-lg overflow-hidden relative" style="height: 500px; display: none;"> | |
| <canvas id="canvas"></canvas> | |
| </div> | |
| <div id="placeholder" class="w-full bg-gray-100 rounded-lg flex flex-col items-center justify-center p-8" style="height: 500px;"> | |
| <i class="fas fa-image text-gray-400 text-6xl mb-4"></i> | |
| <h3 class="text-xl text-gray-600 font-medium mb-2">No Image Loaded</h3> | |
| <p class="text-gray-500 text-center">Please select an image using the button above to begin warping</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Help Modal --> | |
| <div id="help-modal" class="modal fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50 opacity-0 pointer-events-none"> | |
| <div class="modal-content bg-white rounded-xl shadow-xl w-full max-w-md max-h-screen overflow-hidden"> | |
| <div class="p-6 bg-purple-600 text-white"> | |
| <h2 class="text-xl font-bold">How to Use the Warp Tool</h2> | |
| <button id="close-help" class="absolute top-4 right-4 text-white hover:text-purple-200"> | |
| <i class="fas fa-times text-2xl"></i> | |
| </button> | |
| </div> | |
| <div class="p-6 overflow-y-auto"> | |
| <div class="space-y-4"> | |
| <div class="flex items-start"> | |
| <div class="bg-purple-100 text-purple-800 rounded-full w-8 h-8 flex items-center justify-center flex-shrink-0 mr-3"> | |
| <i class="fas fa-upload"></i> | |
| </div> | |
| <div> | |
| <h3 class="font-medium text-gray-800">Uploading Images</h3> | |
| <p class="text-gray-600 mt-1">Tap the "Select Image" button to choose an image from your device. The viewer supports JPG, PNG, and other common image formats.</p> | |
| </div> | |
| </div> | |
| <div class="flex items-start"> | |
| <div class="bg-purple-100 text-purple-800 rounded-full w-8 h-8 flex items-center justify-center flex-shrink-0 mr-3"> | |
| <i class="fas fa-plus-circle"></i> | |
| </div> | |
| <div> | |
| <h3 class="font-medium text-gray-800">Adding Control Points</h3> | |
| <p class="text-gray-600 mt-1">After loading an image, click "Add Point" then tap anywhere on the image to place a control point. Add multiple points to different features.</p> | |
| </div> | |
| </div> | |
| <div class="flex items-start"> | |
| <div class="bg-purple-100 text-purple-800 rounded-full w-8 h-8 flex items-center justify-center flex-shrink-0 mr-3"> | |
| <i class="fas fa-hand-paper"></i> | |
| </div> | |
| <div> | |
| <h3 class="font-medium text-gray-800">Warping the Image</h3> | |
| <p class="text-gray-600 mt-1">Drag any control point to distort the image. The surrounding pixels will stretch naturally while other areas remain mostly unchanged.</p> | |
| </div> | |
| </div> | |
| <div class="flex items-start"> | |
| <div class="bg-purple-100 text-purple-800 rounded-full w-8 h-8 flex items-center justify-center flex-shrink-0 mr-3"> | |
| <i class="fas fa-user-edit"></i> | |
| </div> | |
| <div> | |
| <h3 class="font-medium text-gray-800">Editing Features</h3> | |
| <p class="text-gray-600 mt-1">Perfect for adjusting facial features - make noses bigger, move ears, reshape chins. The edges of the image stay fixed while you edit.</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="p-4 bg-gray-50 flex justify-end"> | |
| <button id="got-it-btn" class="bg-purple-500 hover:bg-purple-600 text-white py-2 px-6 rounded-lg transition duration-200"> | |
| Got it! | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // Elements | |
| const imageUpload = document.getElementById('image-upload'); | |
| const uploadBtn = document.getElementById('upload-btn'); | |
| const addPointBtn = document.getElementById('add-point-btn'); | |
| const imageContainer = document.getElementById('image-container'); | |
| const canvas = document.getElementById('canvas'); | |
| const ctx = canvas.getContext('2d'); | |
| const placeholder = document.getElementById('placeholder'); | |
| const helpBtn = document.getElementById('help-btn'); | |
| const helpModal = document.getElementById('help-modal'); | |
| const closeHelp = document.getElementById('close-help'); | |
| const gotItBtn = document.getElementById('got-it-btn'); | |
| // Image and points data | |
| let img = null; | |
| let originalWidth = 0; | |
| let originalHeight = 0; | |
| let scaleFactor = 1; | |
| const controlPoints = []; | |
| let activePoint = null; | |
| let isAddingPoint = false; | |
| // Initialize canvas | |
| function initCanvas() { | |
| canvas.width = imageContainer.clientWidth; | |
| canvas.height = imageContainer.clientHeight; | |
| drawImage(); | |
| } | |
| // Draw image with current warping | |
| function drawImage() { | |
| if (!img) return; | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| // Calculate scale to fit image to container | |
| const containerRatio = canvas.width / canvas.height; | |
| const imageRatio = img.width / img.height; | |
| let drawWidth, drawHeight, offsetX, offsetY; | |
| if (imageRatio > containerRatio) { | |
| // Image is wider than container relative to height | |
| drawWidth = canvas.width; | |
| drawHeight = canvas.width / imageRatio; | |
| offsetX = 0; | |
| offsetY = (canvas.height - drawHeight) / 2; | |
| } else { | |
| // Image is taller than container relative to width | |
| drawHeight = canvas.height; | |
| drawWidth = canvas.height * imageRatio; | |
| offsetX = (canvas.width - drawWidth) / 2; | |
| offsetY = 0; | |
| } | |
| // Save original dimensions | |
| originalWidth = drawWidth; | |
| originalHeight = drawHeight; | |
| // Draw image | |
| ctx.drawImage(img, offsetX, offsetY, drawWidth, drawHeight); | |
| // Apply warping if we have control points | |
| if (controlPoints.length > 0) { | |
| applyWarping(offsetX, offsetY, drawWidth, drawHeight); | |
| } | |
| // Draw control points | |
| drawControlPoints(); | |
| } | |
| // Apply image warping based on control points | |
| function applyWarping(offsetX, offsetY, width, height) { | |
| // Get image data | |
| const imageData = ctx.getImageData(offsetX, offsetY, width, height); | |
| const data = imageData.data; | |
| const tempCanvas = document.createElement('canvas'); | |
| const tempCtx = tempCanvas.getContext('2d'); | |
| tempCanvas.width = width; | |
| tempCanvas.height = height; | |
| tempCtx.putImageData(imageData, 0, 0); | |
| // For each pixel, calculate displacement based on control points | |
| for (let y = 0; y < height; y++) { | |
| for (let x = 0; x < width; x++) { | |
| const originalX = x; | |
| const originalY = y; | |
| let newX = x; | |
| let newY = y; | |
| // Apply displacement from each control point | |
| for (const point of controlPoints) { | |
| if (point.originalX === undefined) { | |
| point.originalX = point.x; | |
| point.originalY = point.y; | |
| } | |
| const dx = point.x - point.originalX; | |
| const dy = point.y - point.originalY; | |
| // Calculate distance from pixel to control point | |
| const distX = x - point.originalX; | |
| const distY = y - point.originalY; | |
| const distance = Math.sqrt(distX * distX + distY * distY); | |
| // Only affect pixels within a certain radius | |
| if (distance < 150) { // Radius of influence | |
| // Calculate displacement factor (stronger near the point) | |
| const factor = 1 - (distance / 150); | |
| const displacementFactor = factor * factor * 0.8; // Cubic falloff | |
| newX += dx * displacementFactor; | |
| newY += dy * displacementFactor; | |
| } | |
| } | |
| // Get color from original position | |
| const origColor = getPixel(tempCtx, originalX, originalY); | |
| // Set color at new position | |
| if (newX >= 0 && newX < width && newY >= 0 && newY < height) { | |
| const index = (Math.floor(newY) * width + Math.floor(newX)) * 4; | |
| data[index] = origColor.r; | |
| data[index + 1] = origColor.g; | |
| data[index + 2] = origColor.b; | |
| data[index + 3] = origColor.a; | |
| } | |
| } | |
| } | |
| // Put the warped image data back | |
| ctx.putImageData(imageData, offsetX, offsetY); | |
| } | |
| // Helper function to get pixel color | |
| function getPixel(ctx, x, y) { | |
| const pixel = ctx.getImageData(x, y, 1, 1).data; | |
| return { | |
| r: pixel[0], | |
| g: pixel[1], | |
| b: pixel[2], | |
| a: pixel[3] | |
| }; | |
| } | |
| // Draw control points | |
| function drawControlPoints() { | |
| controlPoints.forEach(point => { | |
| const el = point.element || createControlPointElement(point); | |
| point.element = el; | |
| // Position the element | |
| el.style.left = `${point.x}px`; | |
| el.style.top = `${point.y}px`; | |
| // Add to DOM if not already there | |
| if (!el.parentNode) { | |
| imageContainer.appendChild(el); | |
| } | |
| }); | |
| } | |
| // Create a control point DOM element | |
| function createControlPointElement(point) { | |
| const el = document.createElement('div'); | |
| el.className = 'control-point'; | |
| el.dataset.id = point.id; | |
| el.addEventListener('mousedown', (e) => { | |
| e.stopPropagation(); | |
| startDragPoint(point, e.clientX, e.clientY); | |
| }); | |
| el.addEventListener('touchstart', (e) => { | |
| e.stopPropagation(); | |
| startDragPoint(point, e.touches[0].clientX, e.touches[0].clientY); | |
| }); | |
| return el; | |
| } | |
| // Start dragging a control point | |
| function startDragPoint(point, clientX, clientY) { | |
| activePoint = point; | |
| point.startX = clientX; | |
| point.startY = clientY; | |
| point.originalX = point.x; | |
| point.originalY = point.y; | |
| // Highlight active point | |
| if (point.element) { | |
| point.element.classList.add('active'); | |
| } | |
| // Prevent text selection during drag | |
| document.body.style.userSelect = 'none'; | |
| } | |
| // Drag control point | |
| function dragPoint(clientX, clientY) { | |
| if (!activePoint) return; | |
| const dx = clientX - activePoint.startX; | |
| const dy = clientY - activePoint.startY; | |
| activePoint.x = activePoint.originalX + dx; | |
| activePoint.y = activePoint.originalY + dy; | |
| // Redraw image with new warping | |
| drawImage(); | |
| } | |
| // End dragging | |
| function endDragPoint() { | |
| if (activePoint && activePoint.element) { | |
| activePoint.element.classList.remove('active'); | |
| } | |
| activePoint = null; | |
| document.body.style.userSelect = ''; | |
| } | |
| // Add a new control point | |
| function addControlPoint(x, y) { | |
| const point = { | |
| id: Date.now(), | |
| x: x, | |
| y: y, | |
| originalX: x, | |
| originalY: y | |
| }; | |
| controlPoints.push(point); | |
| drawImage(); | |
| } | |
| // Set up event listeners | |
| uploadBtn.addEventListener('click', () => imageUpload.click()); | |
| imageUpload.addEventListener('change', function(e) { | |
| if (e.target.files && e.target.files[0]) { | |
| const reader = new FileReader(); | |
| reader.onload = function(event) { | |
| img = new Image(); | |
| img.onload = function() { | |
| // Show container and hide placeholder | |
| imageContainer.style.display = 'block'; | |
| placeholder.style.display = 'none'; | |
| // Enable add point button | |
| addPointBtn.disabled = false; | |
| // Initialize canvas and draw image | |
| initCanvas(); | |
| }; | |
| img.src = event.target.result; | |
| }; | |
| reader.readAsDataURL(e.target.files[0]); | |
| } | |
| }); | |
| addPointBtn.addEventListener('click', function() { | |
| isAddingPoint = true; | |
| canvas.style.cursor = 'crosshair'; | |
| }); | |
| // Canvas click/touch for adding points | |
| canvas.addEventListener('click', function(e) { | |
| if (!isAddingPoint) return; | |
| const rect = canvas.getBoundingClientRect(); | |
| const x = e.clientX - rect.left; | |
| const y = e.clientY - rect.top; | |
| addControlPoint(x, y); | |
| isAddingPoint = false; | |
| canvas.style.cursor = 'move'; | |
| }); | |
| canvas.addEventListener('touchstart', function(e) { | |
| if (!isAddingPoint) return; | |
| e.preventDefault(); | |
| const rect = canvas.getBoundingClientRect(); | |
| const x = e.touches[0].clientX - rect.left; | |
| const y = e.touches[0].clientY - rect.top; | |
| addControlPoint(x, y); | |
| isAddingPoint = false; | |
| canvas.style.cursor = 'move'; | |
| }, { passive: false }); | |
| // Drag events for control points | |
| document.addEventListener('mousemove', function(e) { | |
| dragPoint(e.clientX, e.clientY); | |
| }); | |
| document.addEventListener('touchmove', function(e) { | |
| if (e.touches.length === 1) { | |
| dragPoint(e.touches[0].clientX, e.touches[0].clientY); | |
| } | |
| }, { passive: false }); | |
| document.addEventListener('mouseup', endDragPoint); | |
| document.addEventListener('touchend', endDragPoint); | |
| // Help modal | |
| helpBtn.addEventListener('click', showHelpModal); | |
| closeHelp.addEventListener('click', hideHelpModal); | |
| gotItBtn.addEventListener('click', hideHelpModal); | |
| // Window resize | |
| window.addEventListener('resize', function() { | |
| if (img) { | |
| initCanvas(); | |
| } | |
| }); | |
| // Modal functions | |
| function showHelpModal() { | |
| helpModal.classList.remove('opacity-0', 'pointer-events-none'); | |
| helpModal.classList.add('opacity-100'); | |
| } | |
| function hideHelpModal() { | |
| helpModal.classList.remove('opacity-100'); | |
| helpModal.classList.add('opacity-0', 'pointer-events-none'); | |
| } | |
| }); | |
| </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=MarkTheArtist/wiggle" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |