Created
December 4, 2025 04:01
-
-
Save princemaple/1e5ec9f526f04632d8b50507b3675115 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!doctype html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>Picture Comparison App</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| </head> | |
| <body class="bg-gray-100 min-h-screen p-8"> | |
| <div class="max-w-7xl mx-auto"> | |
| <h1 class="text-4xl font-bold text-gray-800 mb-8 text-center"> | |
| Picture Comparison App | |
| </h1> | |
| <!-- Canvas Section --> | |
| <div class="bg-white rounded-lg shadow-lg p-6 mb-6"> | |
| <div | |
| class="relative bg-gray-200 rounded-lg overflow-hidden flex items-center justify-center" | |
| style="height: 600px" | |
| > | |
| <canvas id="canvas" class="cursor-move"></canvas> | |
| <!-- Swap Layers Button --> | |
| <button | |
| id="swap-layers" | |
| class="absolute top-4 right-4 bg-purple-500 hover:bg-purple-600 text-white font-semibold py-2 px-4 rounded-lg shadow-lg transition duration-200 flex items-center gap-2" | |
| > | |
| <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path | |
| stroke-linecap="round" | |
| stroke-linejoin="round" | |
| stroke-width="2" | |
| d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4" | |
| ></path> | |
| </svg> | |
| Swap | |
| </button> | |
| </div> | |
| <p class="text-sm text-gray-600 mt-4 text-center"> | |
| Click on a picture control below to select it, then drag on canvas to move or | |
| use mouse wheel to scale | |
| </p> | |
| </div> | |
| <!-- Controls Section --> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| <!-- Picture 1 Controls --> | |
| <div | |
| id="controls1" | |
| class="bg-white rounded-lg shadow-lg p-6 cursor-pointer transition-all duration-200 border-2 border-transparent" | |
| > | |
| <div class="flex items-center justify-between mb-4"> | |
| <h2 class="text-2xl font-semibold text-blue-600">Picture 1</h2> | |
| <button | |
| id="reset1" | |
| class="bg-blue-500 hover:bg-blue-600 text-white font-semibold py-2 px-4 rounded-lg transition duration-200" | |
| > | |
| Reset | |
| </button> | |
| </div> | |
| <input | |
| type="file" | |
| id="file1" | |
| accept="image/*" | |
| class="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100 mb-4" | |
| /> | |
| <div class="grid grid-cols-2 gap-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-2" | |
| >X: <span id="x1-value">0</span>px</label | |
| > | |
| <input | |
| type="range" | |
| id="x1" | |
| min="-500" | |
| max="500" | |
| value="0" | |
| class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-blue-600" | |
| /> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-2" | |
| >Y: <span id="y1-value">0</span>px</label | |
| > | |
| <input | |
| type="range" | |
| id="y1" | |
| min="-500" | |
| max="500" | |
| value="0" | |
| class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-blue-600" | |
| /> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-2" | |
| >Opacity: <span id="opacity1-value">100</span>%</label | |
| > | |
| <input | |
| type="range" | |
| id="opacity1" | |
| min="0" | |
| max="100" | |
| value="100" | |
| class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-blue-600" | |
| /> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-2" | |
| >Scale: <span id="scale1-value">1.0</span>x</label | |
| > | |
| <input | |
| type="range" | |
| id="scale1" | |
| min="10" | |
| max="200" | |
| value="100" | |
| class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-blue-600" | |
| /> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-2" | |
| >Skew X: <span id="skewX1-value">0</span>°</label | |
| > | |
| <input | |
| type="range" | |
| id="skewX1" | |
| min="-45" | |
| max="45" | |
| value="0" | |
| class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-blue-600" | |
| /> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-2" | |
| >Skew Y: <span id="skewY1-value">0</span>°</label | |
| > | |
| <input | |
| type="range" | |
| id="skewY1" | |
| min="-45" | |
| max="45" | |
| value="0" | |
| class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-blue-600" | |
| /> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-2" | |
| >Stretch X: <span id="stretchX1-value">1.0</span>x</label | |
| > | |
| <input | |
| type="range" | |
| id="stretchX1" | |
| min="10" | |
| max="300" | |
| value="100" | |
| class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-blue-600" | |
| /> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-2" | |
| >Stretch Y: <span id="stretchY1-value">1.0</span>x</label | |
| > | |
| <input | |
| type="range" | |
| id="stretchY1" | |
| min="10" | |
| max="300" | |
| value="100" | |
| class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-blue-600" | |
| /> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-2" | |
| >Rotation: <span id="rotation1-value">0</span>°</label | |
| > | |
| <input | |
| type="range" | |
| id="rotation1" | |
| min="-180" | |
| max="180" | |
| value="0" | |
| class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-blue-600" | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Picture 2 Controls --> | |
| <div | |
| id="controls2" | |
| class="bg-white rounded-lg shadow-lg p-6 cursor-pointer transition-all duration-200 border-2 border-transparent" | |
| > | |
| <div class="flex items-center justify-between mb-4"> | |
| <h2 class="text-2xl font-semibold text-green-600">Picture 2</h2> | |
| <button | |
| id="reset2" | |
| class="bg-green-500 hover:bg-green-600 text-white font-semibold py-2 px-4 rounded-lg transition duration-200" | |
| > | |
| Reset | |
| </button> | |
| </div> | |
| <input | |
| type="file" | |
| id="file2" | |
| accept="image/*" | |
| class="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-green-50 file:text-green-700 hover:file:bg-green-100 mb-4" | |
| /> | |
| <div class="grid grid-cols-2 gap-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-2" | |
| >X: <span id="x2-value">0</span>px</label | |
| > | |
| <input | |
| type="range" | |
| id="x2" | |
| min="-500" | |
| max="500" | |
| value="0" | |
| class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-green-600" | |
| /> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-2" | |
| >Y: <span id="y2-value">0</span>px</label | |
| > | |
| <input | |
| type="range" | |
| id="y2" | |
| min="-500" | |
| max="500" | |
| value="0" | |
| class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-green-600" | |
| /> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-2" | |
| >Opacity: <span id="opacity2-value">100</span>%</label | |
| > | |
| <input | |
| type="range" | |
| id="opacity2" | |
| min="0" | |
| max="100" | |
| value="100" | |
| class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-green-600" | |
| /> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-2" | |
| >Scale: <span id="scale2-value">1.0</span>x</label | |
| > | |
| <input | |
| type="range" | |
| id="scale2" | |
| min="10" | |
| max="200" | |
| value="100" | |
| class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-green-600" | |
| /> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-2" | |
| >Skew X: <span id="skewX2-value">0</span>°</label | |
| > | |
| <input | |
| type="range" | |
| id="skewX2" | |
| min="-45" | |
| max="45" | |
| value="0" | |
| class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-green-600" | |
| /> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-2" | |
| >Skew Y: <span id="skewY2-value">0</span>°</label | |
| > | |
| <input | |
| type="range" | |
| id="skewY2" | |
| min="-45" | |
| max="45" | |
| value="0" | |
| class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-green-600" | |
| /> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-2" | |
| >Stretch X: <span id="stretchX2-value">1.0</span>x</label | |
| > | |
| <input | |
| type="range" | |
| id="stretchX2" | |
| min="10" | |
| max="300" | |
| value="100" | |
| class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-green-600" | |
| /> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-2" | |
| >Stretch Y: <span id="stretchY2-value">1.0</span>x</label | |
| > | |
| <input | |
| type="range" | |
| id="stretchY2" | |
| min="10" | |
| max="300" | |
| value="100" | |
| class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-green-600" | |
| /> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-2" | |
| >Rotation: <span id="rotation2-value">0</span>°</label | |
| > | |
| <input | |
| type="range" | |
| id="rotation2" | |
| min="-180" | |
| max="180" | |
| value="0" | |
| class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-green-600" | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const canvas = document.getElementById('canvas'); | |
| const ctx = canvas.getContext('2d'); | |
| // Set canvas size | |
| canvas.width = 800; | |
| canvas.height = 600; | |
| // Image objects | |
| let img1 = null; | |
| let img2 = null; | |
| let img1Loaded = false; | |
| let img2Loaded = false; | |
| // Image properties | |
| let props1 = { | |
| x: 0, | |
| y: 0, | |
| opacity: 1, | |
| scale: 1, | |
| rotation: 0, | |
| skewX: 0, | |
| skewY: 0, | |
| stretchX: 1, | |
| stretchY: 1, | |
| }; | |
| let props2 = { | |
| x: 0, | |
| y: 0, | |
| opacity: 1, | |
| scale: 1, | |
| rotation: 0, | |
| skewX: 0, | |
| skewY: 0, | |
| stretchX: 1, | |
| stretchY: 1, | |
| }; | |
| // Layer order (true = img1 on top, false = img2 on top) | |
| let img1OnTop = false; | |
| // Selection state | |
| let selected = {img1: false, img2: false}; | |
| // Drag state | |
| let isDragging = false; | |
| let dragStartX = 0; | |
| let dragStartY = 0; | |
| // Update UI based on selection | |
| function updateSelectionUI() { | |
| const controls1 = document.getElementById('controls1'); | |
| const controls2 = document.getElementById('controls2'); | |
| if (selected.img1) { | |
| controls1.classList.add('border-blue-500', 'shadow-xl', 'shadow-blue-200'); | |
| } else { | |
| controls1.classList.remove('border-blue-500', 'shadow-xl', 'shadow-blue-200'); | |
| } | |
| if (selected.img2) { | |
| controls2.classList.add('border-green-500', 'shadow-xl', 'shadow-green-200'); | |
| } else { | |
| controls2.classList.remove('border-green-500', 'shadow-xl', 'shadow-green-200'); | |
| } | |
| } | |
| // Selection handlers | |
| document.getElementById('controls1').addEventListener('click', e => { | |
| if (e.ctrlKey || e.metaKey) { | |
| selected.img1 = !selected.img1; | |
| } else { | |
| selected.img1 = true; | |
| selected.img2 = false; | |
| } | |
| updateSelectionUI(); | |
| }); | |
| document.getElementById('controls2').addEventListener('click', e => { | |
| if (e.ctrlKey || e.metaKey) { | |
| selected.img2 = !selected.img2; | |
| } else { | |
| selected.img2 = true; | |
| selected.img1 = false; | |
| } | |
| updateSelectionUI(); | |
| }); | |
| // Canvas click to deselect | |
| canvas.addEventListener('click', e => { | |
| if (!isDragging) { | |
| selected.img1 = false; | |
| selected.img2 = false; | |
| updateSelectionUI(); | |
| } | |
| }); | |
| // Mouse drag handlers | |
| canvas.addEventListener('mousedown', e => { | |
| if (selected.img1 || selected.img2) { | |
| isDragging = true; | |
| dragStartX = e.offsetX; | |
| dragStartY = e.offsetY; | |
| e.preventDefault(); | |
| } | |
| }); | |
| canvas.addEventListener('mousemove', e => { | |
| if (isDragging) { | |
| const deltaX = e.offsetX - dragStartX; | |
| const deltaY = e.offsetY - dragStartY; | |
| if (selected.img1) { | |
| props1.x += deltaX; | |
| props1.y += deltaY; | |
| updateSliders1(); | |
| } | |
| if (selected.img2) { | |
| props2.x += deltaX; | |
| props2.y += deltaY; | |
| updateSliders2(); | |
| } | |
| dragStartX = e.offsetX; | |
| dragStartY = e.offsetY; | |
| draw(); | |
| e.preventDefault(); | |
| } | |
| }); | |
| canvas.addEventListener('mouseup', () => { | |
| isDragging = false; | |
| }); | |
| canvas.addEventListener('mouseleave', () => { | |
| isDragging = false; | |
| }); | |
| // Mouse wheel handler | |
| canvas.addEventListener('wheel', e => { | |
| e.preventDefault(); | |
| const delta = -e.deltaY * 0.0002; | |
| if (selected.img1) { | |
| props1.scale = Math.max(0.1, Math.min(2, props1.scale + delta)); | |
| updateSliders1(); | |
| } | |
| if (selected.img2) { | |
| props2.scale = Math.max(0.1, Math.min(2, props2.scale + delta)); | |
| updateSliders2(); | |
| } | |
| draw(); | |
| }); | |
| // File input handlers | |
| document.getElementById('file1').addEventListener('change', e => { | |
| const file = e.target.files[0]; | |
| if (file) { | |
| const reader = new FileReader(); | |
| reader.onload = event => { | |
| img1 = new Image(); | |
| img1.onload = () => { | |
| img1Loaded = true; | |
| draw(); | |
| }; | |
| img1.src = event.target.result; | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| e.stopPropagation(); | |
| }); | |
| document.getElementById('file2').addEventListener('change', e => { | |
| const file = e.target.files[0]; | |
| if (file) { | |
| const reader = new FileReader(); | |
| reader.onload = event => { | |
| img2 = new Image(); | |
| img2.onload = () => { | |
| img2Loaded = true; | |
| draw(); | |
| }; | |
| img2.src = event.target.result; | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| e.stopPropagation(); | |
| }); | |
| // Update slider values for Picture 1 | |
| function updateSliders1() { | |
| document.getElementById('x1').value = props1.x; | |
| document.getElementById('y1').value = props1.y; | |
| document.getElementById('scale1').value = props1.scale * 100; | |
| document.getElementById('rotation1').value = props1.rotation; | |
| document.getElementById('skewX1').value = props1.skewX; | |
| document.getElementById('skewY1').value = props1.skewY; | |
| document.getElementById('stretchX1').value = props1.stretchX * 100; | |
| document.getElementById('stretchY1').value = props1.stretchY * 100; | |
| document.getElementById('x1-value').textContent = Math.round(props1.x); | |
| document.getElementById('y1-value').textContent = Math.round(props1.y); | |
| document.getElementById('scale1-value').textContent = props1.scale.toFixed(2); | |
| document.getElementById('rotation1-value').textContent = Math.round( | |
| props1.rotation, | |
| ); | |
| document.getElementById('skewX1-value').textContent = Math.round(props1.skewX); | |
| document.getElementById('skewY1-value').textContent = Math.round(props1.skewY); | |
| document.getElementById('stretchX1-value').textContent = props1.stretchX.toFixed(2); | |
| document.getElementById('stretchY1-value').textContent = props1.stretchY.toFixed(2); | |
| } | |
| // Update slider values for Picture 2 | |
| function updateSliders2() { | |
| document.getElementById('x2').value = props2.x; | |
| document.getElementById('y2').value = props2.y; | |
| document.getElementById('scale2').value = props2.scale * 100; | |
| document.getElementById('rotation2').value = props2.rotation; | |
| document.getElementById('skewX2').value = props2.skewX; | |
| document.getElementById('skewY2').value = props2.skewY; | |
| document.getElementById('stretchX2').value = props2.stretchX * 100; | |
| document.getElementById('stretchY2').value = props2.stretchY * 100; | |
| document.getElementById('x2-value').textContent = Math.round(props2.x); | |
| document.getElementById('y2-value').textContent = Math.round(props2.y); | |
| document.getElementById('scale2-value').textContent = props2.scale.toFixed(2); | |
| document.getElementById('rotation2-value').textContent = Math.round( | |
| props2.rotation, | |
| ); | |
| document.getElementById('skewX2-value').textContent = Math.round(props2.skewX); | |
| document.getElementById('skewY2-value').textContent = Math.round(props2.skewY); | |
| document.getElementById('stretchX2-value').textContent = props2.stretchX.toFixed(2); | |
| document.getElementById('stretchY2-value').textContent = props2.stretchY.toFixed(2); | |
| } | |
| // Control handlers for Picture 1 | |
| document.getElementById('x1').addEventListener('input', e => { | |
| props1.x = parseInt(e.target.value); | |
| document.getElementById('x1-value').textContent = props1.x; | |
| draw(); | |
| e.stopPropagation(); | |
| }); | |
| document.getElementById('y1').addEventListener('input', e => { | |
| props1.y = parseInt(e.target.value); | |
| document.getElementById('y1-value').textContent = props1.y; | |
| draw(); | |
| e.stopPropagation(); | |
| }); | |
| document.getElementById('opacity1').addEventListener('input', e => { | |
| props1.opacity = parseInt(e.target.value) / 100; | |
| document.getElementById('opacity1-value').textContent = e.target.value; | |
| draw(); | |
| e.stopPropagation(); | |
| }); | |
| document.getElementById('scale1').addEventListener('input', e => { | |
| props1.scale = parseInt(e.target.value) / 100; | |
| document.getElementById('scale1-value').textContent = props1.scale.toFixed(2); | |
| draw(); | |
| e.stopPropagation(); | |
| }); | |
| document.getElementById('rotation1').addEventListener('input', e => { | |
| props1.rotation = parseInt(e.target.value); | |
| document.getElementById('rotation1-value').textContent = props1.rotation; | |
| draw(); | |
| e.stopPropagation(); | |
| }); | |
| document.getElementById('skewX1').addEventListener('input', e => { | |
| props1.skewX = parseInt(e.target.value); | |
| document.getElementById('skewX1-value').textContent = props1.skewX; | |
| draw(); | |
| e.stopPropagation(); | |
| }); | |
| document.getElementById('skewY1').addEventListener('input', e => { | |
| props1.skewY = parseInt(e.target.value); | |
| document.getElementById('skewY1-value').textContent = props1.skewY; | |
| draw(); | |
| e.stopPropagation(); | |
| }); | |
| document.getElementById('stretchX1').addEventListener('input', e => { | |
| props1.stretchX = parseInt(e.target.value) / 100; | |
| document.getElementById('stretchX1-value').textContent = props1.stretchX.toFixed(2); | |
| draw(); | |
| e.stopPropagation(); | |
| }); | |
| document.getElementById('stretchY1').addEventListener('input', e => { | |
| props1.stretchY = parseInt(e.target.value) / 100; | |
| document.getElementById('stretchY1-value').textContent = props1.stretchY.toFixed(2); | |
| draw(); | |
| e.stopPropagation(); | |
| }); | |
| // Control handlers for Picture 2 | |
| document.getElementById('x2').addEventListener('input', e => { | |
| props2.x = parseInt(e.target.value); | |
| document.getElementById('x2-value').textContent = props2.x; | |
| draw(); | |
| e.stopPropagation(); | |
| }); | |
| document.getElementById('y2').addEventListener('input', e => { | |
| props2.y = parseInt(e.target.value); | |
| document.getElementById('y2-value').textContent = props2.y; | |
| draw(); | |
| e.stopPropagation(); | |
| }); | |
| document.getElementById('opacity2').addEventListener('input', e => { | |
| props2.opacity = parseInt(e.target.value) / 100; | |
| document.getElementById('opacity2-value').textContent = e.target.value; | |
| draw(); | |
| e.stopPropagation(); | |
| }); | |
| document.getElementById('scale2').addEventListener('input', e => { | |
| props2.scale = parseInt(e.target.value) / 100; | |
| document.getElementById('scale2-value').textContent = props2.scale.toFixed(2); | |
| draw(); | |
| e.stopPropagation(); | |
| }); | |
| document.getElementById('rotation2').addEventListener('input', e => { | |
| props2.rotation = parseInt(e.target.value); | |
| document.getElementById('rotation2-value').textContent = props2.rotation; | |
| draw(); | |
| e.stopPropagation(); | |
| }); | |
| document.getElementById('skewX2').addEventListener('input', e => { | |
| props2.skewX = parseInt(e.target.value); | |
| document.getElementById('skewX2-value').textContent = props2.skewX; | |
| draw(); | |
| e.stopPropagation(); | |
| }); | |
| document.getElementById('skewY2').addEventListener('input', e => { | |
| props2.skewY = parseInt(e.target.value); | |
| document.getElementById('skewY2-value').textContent = props2.skewY; | |
| draw(); | |
| e.stopPropagation(); | |
| }); | |
| document.getElementById('stretchX2').addEventListener('input', e => { | |
| props2.stretchX = parseInt(e.target.value) / 100; | |
| document.getElementById('stretchX2-value').textContent = props2.stretchX.toFixed(2); | |
| draw(); | |
| e.stopPropagation(); | |
| }); | |
| document.getElementById('stretchY2').addEventListener('input', e => { | |
| props2.stretchY = parseInt(e.target.value) / 100; | |
| document.getElementById('stretchY2-value').textContent = props2.stretchY.toFixed(2); | |
| draw(); | |
| e.stopPropagation(); | |
| }); | |
| // Reset buttons | |
| document.getElementById('reset1').addEventListener('click', e => { | |
| props1 = {x: 0, y: 0, opacity: 1, scale: 1, rotation: 0, skewX: 0, skewY: 0, stretchX: 1, stretchY: 1}; | |
| updateSliders1(); | |
| document.getElementById('opacity1').value = 100; | |
| document.getElementById('opacity1-value').textContent = 100; | |
| draw(); | |
| e.stopPropagation(); | |
| }); | |
| document.getElementById('reset2').addEventListener('click', e => { | |
| props2 = {x: 0, y: 0, opacity: 1, scale: 1, rotation: 0, skewX: 0, skewY: 0, stretchX: 1, stretchY: 1}; | |
| updateSliders2(); | |
| document.getElementById('opacity2').value = 100; | |
| document.getElementById('opacity2-value').textContent = 100; | |
| draw(); | |
| e.stopPropagation(); | |
| }); | |
| // Swap layers button | |
| document.getElementById('swap-layers').addEventListener('click', () => { | |
| img1OnTop = !img1OnTop; | |
| draw(); | |
| }); | |
| // Draw function | |
| function draw() { | |
| // Clear canvas | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| // Fill with white background | |
| ctx.fillStyle = '#ffffff'; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| // Draw images in order | |
| if (img1OnTop) { | |
| drawImage(img2, props2); | |
| drawImage(img1, props1); | |
| } else { | |
| drawImage(img1, props1); | |
| drawImage(img2, props2); | |
| } | |
| } | |
| function drawImage(img, props) { | |
| if (!img) return; | |
| ctx.save(); | |
| ctx.globalAlpha = props.opacity; | |
| // Calculate scaled dimensions with stretch | |
| const scaledWidth = img.width * props.scale * props.stretchX; | |
| const scaledHeight = img.height * props.scale * props.stretchY; | |
| // Center the image on canvas and apply offset | |
| const x = canvas.width / 2 + props.x; | |
| const y = canvas.height / 2 + props.y; | |
| // Move to the center point, apply transformations | |
| ctx.translate(x, y); | |
| ctx.rotate((props.rotation * Math.PI) / 180); | |
| ctx.transform( | |
| 1, | |
| (props.skewY * Math.PI) / 180, | |
| (props.skewX * Math.PI) / 180, | |
| 1, | |
| 0, | |
| 0, | |
| ); | |
| // Draw image centered at the transformation point | |
| ctx.drawImage(img, -scaledWidth / 2, -scaledHeight / 2, scaledWidth, scaledHeight); | |
| ctx.restore(); | |
| } | |
| // Initialize | |
| updateSelectionUI(); | |
| draw(); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment