| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Image Animator</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@3.18.0/dist/tf.min.js"></script> |
| <script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/coco-ssd@2.2.2/dist/coco-ssd.min.js"></script> |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.11.4/gsap.min.js"></script> |
| <style> |
| .canvas-container { |
| position: relative; |
| margin: 0 auto; |
| max-width: 100%; |
| } |
| #imageCanvas, #detectionCanvas { |
| position: absolute; |
| top: 0; |
| left: 0; |
| max-width: 100%; |
| height: auto; |
| } |
| #detectionCanvas { |
| pointer-events: none; |
| } |
| .object-box { |
| position: absolute; |
| border: 2px dashed rgba(255, 255, 255, 0.7); |
| background-color: rgba(0, 0, 0, 0.3); |
| display: flex; |
| align-items: flex-end; |
| justify-content: center; |
| color: white; |
| font-weight: bold; |
| padding-bottom: 5px; |
| cursor: pointer; |
| transition: all 0.3s; |
| } |
| .object-box:hover { |
| background-color: rgba(0, 100, 255, 0.5); |
| } |
| .object-box.selected { |
| border: 2px solid #3B82F6; |
| background-color: rgba(59, 130, 246, 0.5); |
| z-index: 10; |
| } |
| .animation-preview { |
| border: 2px solid #3B82F6; |
| box-shadow: 0 0 20px rgba(59, 130, 246, 0.5); |
| } |
| .tooltip { |
| position: relative; |
| display: inline-block; |
| } |
| .tooltip .tooltiptext { |
| visibility: hidden; |
| width: 200px; |
| background-color: #333; |
| color: #fff; |
| text-align: center; |
| border-radius: 6px; |
| padding: 5px; |
| position: absolute; |
| z-index: 1; |
| bottom: 125%; |
| left: 50%; |
| transform: translateX(-50%); |
| opacity: 0; |
| transition: opacity 0.3s; |
| } |
| .tooltip:hover .tooltiptext { |
| visibility: visible; |
| opacity: 1; |
| } |
| @keyframes pulse { |
| 0% { transform: scale(1); } |
| 50% { transform: scale(1.05); } |
| 100% { transform: scale(1); } |
| } |
| .pulse { |
| animation: pulse 2s infinite; |
| } |
| </style> |
| </head> |
| <body class="bg-gray-900 text-white min-h-screen"> |
| <div class="container mx-auto px-4 py-8"> |
| <header class="text-center mb-8"> |
| <h1 class="text-4xl font-bold text-blue-400 mb-2">Image Animator</h1> |
| <p class="text-gray-400">Upload an image, detect objects, and bring them to life with animations!</p> |
| </header> |
|
|
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-8"> |
| |
| <div class="bg-gray-800 rounded-lg p-6 shadow-lg"> |
| <h2 class="text-xl font-semibold mb-4 text-blue-300">1. Upload Image</h2> |
| <div class="border-2 border-dashed border-gray-600 rounded-lg p-6 text-center mb-4 hover:border-blue-400 transition-colors"> |
| <input type="file" id="imageUpload" accept="image/*" class="hidden"> |
| <label for="imageUpload" class="cursor-pointer block"> |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 mx-auto text-blue-400 mb-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /> |
| </svg> |
| <p class="text-gray-300">Click to upload an image</p> |
| <p class="text-sm text-gray-500 mt-1">Supports JPG, PNG, WEBP</p> |
| </label> |
| </div> |
| <div class="flex justify-between"> |
| <button id="detectBtn" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed" disabled> |
| Detect Objects |
| </button> |
| <button id="resetBtn" class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed" disabled> |
| Reset |
| </button> |
| </div> |
| </div> |
|
|
| |
| <div class="bg-gray-800 rounded-lg p-6 shadow-lg"> |
| <h2 class="text-xl font-semibold mb-4 text-blue-300">2. Detected Objects</h2> |
| <div id="objectList" class="max-h-64 overflow-y-auto mb-4"> |
| <p class="text-gray-500 text-center py-8">No objects detected yet</p> |
| </div> |
| <div class="flex items-center justify-between"> |
| <span class="text-sm text-gray-400">Selected: <span id="selectedObject">None</span></span> |
| <button id="animateBtn" class="bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed" disabled> |
| Animate Selected |
| </button> |
| </div> |
| </div> |
|
|
| |
| <div class="bg-gray-800 rounded-lg p-6 shadow-lg"> |
| <h2 class="text-xl font-semibold mb-4 text-blue-300">3. Animation Controls</h2> |
| <div class="space-y-4"> |
| <div> |
| <label class="block text-sm font-medium text-gray-400 mb-1">Animation Type</label> |
| <select id="animationType" class="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2 text-white"> |
| <option value="bounce">Bounce</option> |
| <option value="shake">Shake</option> |
| <option value="float">Float</option> |
| <option value="spin">Spin</option> |
| <option value="pulse">Pulse</option> |
| <option value="slide">Slide</option> |
| </select> |
| </div> |
| <div> |
| <label class="block text-sm font-medium text-gray-400 mb-1">Duration (seconds)</label> |
| <input type="range" id="duration" min="0.5" max="5" step="0.5" value="2" class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer"> |
| <span id="durationValue" class="text-sm text-gray-400">2.0s</span> |
| </div> |
| <div> |
| <label class="block text-sm font-medium text-gray-400 mb-1">Intensity</label> |
| <input type="range" id="intensity" min="1" max="10" step="1" value="5" class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer"> |
| <span id="intensityValue" class="text-sm text-gray-400">5</span> |
| </div> |
| <div class="flex space-x-2"> |
| <button id="previewBtn" class="flex-1 bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed" disabled> |
| Preview |
| </button> |
| <button id="saveBtn" class="flex-1 bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed" disabled> |
| Save Animation |
| </button> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="mt-8 bg-gray-800 rounded-lg p-6 shadow-lg"> |
| <h2 class="text-xl font-semibold mb-4 text-blue-300">Preview</h2> |
| <div class="canvas-container"> |
| <canvas id="imageCanvas" class="rounded-lg shadow-lg"></canvas> |
| <canvas id="detectionCanvas" class="rounded-lg"></canvas> |
| <div id="objectBoxesContainer"></div> |
| </div> |
| </div> |
|
|
| |
| <div id="savedAnimations" class="mt-8 bg-gray-800 rounded-lg p-6 shadow-lg hidden"> |
| <h2 class="text-xl font-semibold mb-4 text-blue-300">Saved Animations</h2> |
| <div id="savedAnimationsList" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"> |
| |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| |
| const imageUpload = document.getElementById('imageUpload'); |
| const detectBtn = document.getElementById('detectBtn'); |
| const resetBtn = document.getElementById('resetBtn'); |
| const animateBtn = document.getElementById('animateBtn'); |
| const previewBtn = document.getElementById('previewBtn'); |
| const saveBtn = document.getElementById('saveBtn'); |
| const objectList = document.getElementById('objectList'); |
| const selectedObjectDisplay = document.getElementById('selectedObject'); |
| const animationType = document.getElementById('animationType'); |
| const duration = document.getElementById('duration'); |
| const durationValue = document.getElementById('durationValue'); |
| const intensity = document.getElementById('intensity'); |
| const intensityValue = document.getElementById('intensityValue'); |
| const imageCanvas = document.getElementById('imageCanvas'); |
| const detectionCanvas = document.getElementById('detectionCanvas'); |
| const objectBoxesContainer = document.getElementById('objectBoxesContainer'); |
| const savedAnimations = document.getElementById('savedAnimations'); |
| const savedAnimationsList = document.getElementById('savedAnimationsList'); |
| |
| |
| const imageCtx = imageCanvas.getContext('2d'); |
| const detectionCtx = detectionCanvas.getContext('2d'); |
| |
| |
| let model = null; |
| let detectedObjects = []; |
| let selectedObject = null; |
| let originalImage = null; |
| let currentAnimations = []; |
| let savedAnimationItems = []; |
| |
| |
| async function initModel() { |
| try { |
| model = await cocoSsd.load(); |
| console.log('Model loaded successfully'); |
| } catch (error) { |
| console.error('Error loading model:', error); |
| alert('Failed to load the object detection model. Please try again later.'); |
| } |
| } |
| |
| |
| imageUpload.addEventListener('change', function(e) { |
| if (e.target.files && e.target.files[0]) { |
| const reader = new FileReader(); |
| reader.onload = function(event) { |
| originalImage = new Image(); |
| originalImage.onload = function() { |
| |
| const maxWidth = 800; |
| const ratio = Math.min(maxWidth / originalImage.width, 1); |
| const width = originalImage.width * ratio; |
| const height = originalImage.height * ratio; |
| |
| imageCanvas.width = width; |
| imageCanvas.height = height; |
| detectionCanvas.width = width; |
| detectionCanvas.height = height; |
| |
| |
| imageCtx.drawImage(originalImage, 0, 0, width, height); |
| |
| |
| detectBtn.disabled = false; |
| resetBtn.disabled = false; |
| |
| |
| clearDetections(); |
| selectedObject = null; |
| selectedObjectDisplay.textContent = 'None'; |
| animateBtn.disabled = true; |
| previewBtn.disabled = true; |
| saveBtn.disabled = true; |
| }; |
| originalImage.src = event.target.result; |
| }; |
| reader.readAsDataURL(e.target.files[0]); |
| } |
| }); |
| |
| |
| detectBtn.addEventListener('click', async function() { |
| if (!originalImage) return; |
| |
| detectBtn.disabled = true; |
| detectBtn.textContent = 'Detecting...'; |
| |
| try { |
| |
| const predictions = await model.detect(imageCanvas); |
| detectedObjects = predictions; |
| |
| |
| detectionCtx.clearRect(0, 0, detectionCanvas.width, detectionCanvas.height); |
| objectBoxesContainer.innerHTML = ''; |
| objectList.innerHTML = ''; |
| |
| if (predictions.length === 0) { |
| objectList.innerHTML = '<p class="text-gray-500 text-center py-8">No objects detected</p>'; |
| return; |
| } |
| |
| |
| predictions.forEach((obj, index) => { |
| |
| detectionCtx.strokeStyle = '#3B82F6'; |
| detectionCtx.lineWidth = 2; |
| detectionCtx.strokeRect(obj.bbox[0], obj.bbox[1], obj.bbox[2], obj.bbox[3]); |
| detectionCtx.fillStyle = '#3B82F6'; |
| detectionCtx.font = '12px Arial'; |
| detectionCtx.fillText(`${obj.class} (${Math.round(obj.score * 100)}%)`, obj.bbox[0], obj.bbox[1] > 10 ? obj.bbox[1] - 5 : 10); |
| |
| |
| const box = document.createElement('div'); |
| box.className = 'object-box'; |
| box.style.left = `${obj.bbox[0]}px`; |
| box.style.top = `${obj.bbox[1]}px`; |
| box.style.width = `${obj.bbox[2]}px`; |
| box.style.height = `${obj.bbox[3]}px`; |
| box.dataset.index = index; |
| box.innerHTML = `<span class="text-xs">${obj.class}</span>`; |
| |
| box.addEventListener('click', function() { |
| selectObject(index); |
| }); |
| |
| objectBoxesContainer.appendChild(box); |
| |
| |
| const listItem = document.createElement('div'); |
| listItem.className = 'flex items-center justify-between p-2 hover:bg-gray-700 rounded-lg cursor-pointer'; |
| listItem.dataset.index = index; |
| listItem.innerHTML = ` |
| <div class="flex items-center"> |
| <div class="w-3 h-3 rounded-full bg-blue-400 mr-2"></div> |
| <span>${obj.class}</span> |
| </div> |
| <span class="text-xs text-gray-400">${Math.round(obj.score * 100)}%</span> |
| `; |
| |
| listItem.addEventListener('click', function() { |
| selectObject(index); |
| }); |
| |
| objectList.appendChild(listItem); |
| }); |
| |
| |
| animateBtn.disabled = false; |
| |
| } catch (error) { |
| console.error('Detection error:', error); |
| alert('Error detecting objects. Please try again.'); |
| } finally { |
| detectBtn.disabled = false; |
| detectBtn.textContent = 'Detect Objects'; |
| } |
| }); |
| |
| |
| function selectObject(index) { |
| |
| if (selectedObject !== null) { |
| const prevBoxes = document.querySelectorAll('.object-box, #objectList div'); |
| prevBoxes[selectedObject].classList.remove('selected'); |
| } |
| |
| |
| selectedObject = index; |
| const boxes = document.querySelectorAll('.object-box, #objectList div'); |
| boxes[index].classList.add('selected'); |
| |
| |
| selectedObjectDisplay.textContent = detectedObjects[index].class; |
| previewBtn.disabled = false; |
| saveBtn.disabled = false; |
| } |
| |
| |
| resetBtn.addEventListener('click', function() { |
| if (!originalImage) return; |
| |
| |
| imageCtx.clearRect(0, 0, imageCanvas.width, imageCanvas.height); |
| detectionCtx.clearRect(0, 0, detectionCanvas.width, detectionCanvas.height); |
| |
| |
| imageCtx.drawImage(originalImage, 0, 0, imageCanvas.width, image |
| </html> |