wiggle / index.html
MarkTheArtist's picture
Add 2 files
0ac8f2f verified
<!DOCTYPE html>
<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>