| <!DOCTYPE html> |
| <html lang="ja"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>画像編集ツール</title> |
| <style> |
| body { |
| font-family: Arial, sans-serif; |
| margin: 0; |
| padding: 0; |
| overflow: hidden; |
| height: 100vh; |
| --bg-color: #f0f0f0; |
| } |
| #preview-container { |
| position: fixed; |
| top: 0; |
| left: 0; |
| width: 100%; |
| height: 100%; |
| background-color: var(--bg-color); |
| overflow: hidden; |
| position: relative; |
| } |
| #preview-image { |
| position: absolute; |
| cursor: move; |
| transform-origin: center center; |
| display: none; |
| } |
| .control-point { |
| position: absolute; |
| width: 10px; |
| height: 10px; |
| background-color: #4285f4; |
| border-radius: 50%; |
| cursor: pointer; |
| z-index: 10; |
| display: none; |
| } |
| .rotate-handle { |
| position: absolute; |
| width: 30px; |
| height: 30px; |
| background-color: #4285f4; |
| color: white; |
| border-radius: 50%; |
| display: none; |
| align-items: center; |
| justify-content: center; |
| font-size: 18px; |
| cursor: pointer; |
| z-index: 10; |
| font-family: sans-serif; |
| transform: translate(-20%, 0%); |
| } |
| .settings-panel { |
| position: fixed; |
| top: 20px; |
| right: 20px; |
| width: 300px; |
| background-color: white; |
| border-radius: 8px; |
| box-shadow: 0 0 15px rgba(0,0,0,0.2); |
| z-index: 100; |
| overflow: hidden; |
| } |
| .panel-header { |
| padding: 10px 15px; |
| background-color: #4285f4; |
| color: white; |
| cursor: move; |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| } |
| .panel-title { |
| font-weight: bold; |
| } |
| .toggle-panel { |
| background: none; |
| border: none; |
| color: white; |
| font-size: 16px; |
| cursor: pointer; |
| } |
| .panel-content { |
| padding: 15px; |
| transition: all 0.3s ease; |
| } |
| .panel-collapsed .panel-content { |
| display: none; |
| } |
| .form-group { |
| display: flex; |
| margin-bottom: 15px; |
| align-items: center; |
| } |
| .form-group label { |
| width: 80px; |
| font-weight: bold; |
| } |
| .form-group input { |
| width: 70px; |
| padding: 5px; |
| margin-right: 10px; |
| } |
| button { |
| padding: 8px 15px; |
| background-color: #4285f4; |
| color: white; |
| border: none; |
| border-radius: 4px; |
| cursor: pointer; |
| font-size: 14px; |
| margin-right: 5px; |
| } |
| button:hover { |
| background-color: #3367d6; |
| } |
| .upload-section { |
| margin-bottom: 20px; |
| padding: 15px; |
| border: 2px dashed #ccc; |
| border-radius: 8px; |
| text-align: center; |
| } |
| .fullscreen-mode { |
| position: fixed; |
| top: 0; |
| left: 0; |
| width: 100%; |
| height: 100%; |
| background-color: inherit; |
| z-index: 1000; |
| cursor: none !important; |
| } |
| .close-fullscreen { |
| position: fixed; |
| top: 20px; |
| right: 20px; |
| color: #333; |
| font-size: 30px; |
| cursor: pointer; |
| z-index: 1001; |
| background-color: rgba(255,255,255,0.7); |
| width: 40px; |
| height: 40px; |
| border-radius: 50%; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| box-shadow: 0 0 5px rgba(0,0,0,0.2); |
| } |
| .color-picker { |
| display: flex; |
| align-items: center; |
| margin-bottom: 15px; |
| } |
| .color-picker label { |
| width: 80px; |
| font-weight: bold; |
| } |
| .color-picker input { |
| width: 60px; |
| height: 30px; |
| padding: 0; |
| border: 1px solid #ddd; |
| } |
| .hidden { |
| display: none !important; |
| } |
| .flip-buttons { |
| display: flex; |
| margin-bottom: 15px; |
| } |
| .flip-buttons button { |
| flex: 1; |
| } |
| .pointer-lock { |
| cursor: none !important; |
| } |
| </style> |
| </head> |
| <body> |
| <div id="preview-container"> |
| <img id="preview-image" style="-webkit-user-drag: none;"> |
| <div class="rotate-handle" id="rotate-handle">↺</div> |
| |
| </div> |
|
|
| <div class="settings-panel" id="settings-panel"> |
| <div class="panel-header" id="panel-header"> |
| <span class="panel-title">画像設定</span> |
| <button class="toggle-panel" id="toggle-panel">−</button> |
| </div> |
| <div class="panel-content"> |
| <div class="upload-section"> |
| <input type="file" id="image-upload" accept="image/*"> |
| <p>画像をドラッグ&ドロップ</p> |
| </div> |
| |
| <div class="color-picker"> |
| <label>背景色:</label> |
| <input type="color" id="bg-color" value="#f0f0f0"> |
| </div> |
| |
| <div class="form-group"> |
| <label>X位置:</label> |
| <input type="number" id="pos-x"> |
| <label>Y位置:</label> |
| <input type="number" id="pos-y"> |
| </div> |
| <div class="form-group"> |
| <label>幅:</label> |
| <input type="number" id="width"> |
| <label>高さ:</label> |
| <input type="number" id="height"> |
| </div> |
| <div class="form-group"> |
| <label>回転:</label> |
| <input type="number" id="rotation" value="0"> |
| <span>度</span> |
| </div> |
| |
| <div class="flip-buttons"> |
| <button id="flip-horizontal">左右反転</button> |
| <button id="flip-vertical">上下反転</button> |
| </div> |
| |
| <button id="ok-button">全画面表示</button> |
| </div> |
| </div> |
|
|
| <script> |
| document.addEventListener('DOMContentLoaded', function() { |
| const uploadInput = document.getElementById('image-upload'); |
| const previewContainer = document.getElementById('preview-container'); |
| const previewImage = document.getElementById('preview-image'); |
| const rotateHandle = document.getElementById('rotate-handle'); |
| const posXInput = document.getElementById('pos-x'); |
| const posYInput = document.getElementById('pos-y'); |
| const widthInput = document.getElementById('width'); |
| const heightInput = document.getElementById('height'); |
| const rotationInput = document.getElementById('rotation'); |
| const okButton = document.getElementById('ok-button'); |
| const bgColorInput = document.getElementById('bg-color'); |
| const flipHorizontalBtn = document.getElementById('flip-horizontal'); |
| const flipVerticalBtn = document.getElementById('flip-vertical'); |
| const settingsPanel = document.getElementById('settings-panel'); |
| const panelHeader = document.getElementById('panel-header'); |
| const togglePanel = document.getElementById('toggle-panel'); |
| |
| let isDragging = false; |
| let activeControlPoint = null; |
| let isRotating = false; |
| let isDraggingPanel = false; |
| let startX, startY; |
| let startWidth, startHeight; |
| let startRotation; |
| let startImageX, startImageY; |
| let startPanelX, startPanelY; |
| let aspectRatio = 1; |
| let isFullscreen = false; |
| let scaleX = 1; |
| let scaleY = 1; |
| |
| |
| bgColorInput.addEventListener('input', function() { |
| const bgColor = this.value; |
| previewContainer.style.backgroundColor = bgColor; |
| document.documentElement.style.setProperty('--bg-color', bgColor); |
| }); |
| |
| |
| const ControlPointType = { |
| TOP_LEFT: 'top-left', |
| TOP_CENTER: 'top-center', |
| TOP_RIGHT: 'top-right', |
| MIDDLE_LEFT: 'middle-left', |
| MIDDLE_RIGHT: 'middle-right', |
| BOTTOM_LEFT: 'bottom-left', |
| BOTTOM_CENTER: 'bottom-center', |
| BOTTOM_RIGHT: 'bottom-right' |
| }; |
| |
| |
| uploadInput.addEventListener('change', handleImageUpload); |
| previewContainer.addEventListener('dragover', function(e) { |
| e.preventDefault(); |
| e.stopPropagation(); |
| previewContainer.style.backgroundColor = '#e0e0e0'; |
| }); |
| previewContainer.addEventListener('dragleave', function(e) { |
| e.preventDefault(); |
| e.stopPropagation(); |
| previewContainer.style.backgroundColor = bgColorInput.value; |
| }); |
| previewContainer.addEventListener('drop', function(e) { |
| e.preventDefault(); |
| e.stopPropagation(); |
| previewContainer.style.backgroundColor = bgColorInput.value; |
| if (e.dataTransfer.files.length) { |
| uploadInput.files = e.dataTransfer.files; |
| handleImageUpload(); |
| } |
| }); |
| |
| function handleImageUpload() { |
| const file = uploadInput.files[0]; |
| if (!file) return; |
| |
| const reader = new FileReader(); |
| reader.onload = function(e) { |
| previewImage.src = e.target.result; |
| previewImage.style.display = 'block'; |
| rotateHandle.style.display = 'block'; |
| |
| previewImage.onload = function() { |
| |
| const containerWidth = previewContainer.clientWidth; |
| const containerHeight = previewContainer.clientHeight; |
| const imgWidth = previewImage.naturalWidth; |
| const imgHeight = previewImage.naturalHeight; |
| |
| aspectRatio = imgWidth / imgHeight; |
| |
| |
| let initWidth = Math.min(imgWidth, containerWidth * 0.8); |
| let initHeight = initWidth / aspectRatio; |
| |
| if (initHeight > containerHeight * 0.8) { |
| initHeight = containerHeight * 0.8; |
| initWidth = initHeight * aspectRatio; |
| } |
| |
| |
| const initX = (containerWidth - initWidth) / 2; |
| const initY = (containerHeight - initHeight) / 2; |
| |
| updateImageStyle(initX, initY, initWidth, initHeight, 0); |
| updateFormInputs(initX, initY, initWidth, initHeight, 0); |
| createControlPoints(); |
| |
| |
| document.querySelectorAll('.control-point').forEach(point => { |
| point.style.display = 'block'; |
| }); |
| }; |
| }; |
| reader.readAsDataURL(file); |
| } |
| |
| |
| function createControlPoints() { |
| |
| document.querySelectorAll('.control-point').forEach(el => el.remove()); |
| |
| |
| const positions = [ |
| { type: ControlPointType.TOP_LEFT, left: 0, top: 0 }, |
| { type: ControlPointType.TOP_CENTER, left: 50, top: 0 }, |
| { type: ControlPointType.TOP_RIGHT, left: 100, top: 0 }, |
| { type: ControlPointType.MIDDLE_LEFT, left: 0, top: 50 }, |
| { type: ControlPointType.MIDDLE_RIGHT, left: 100, top: 50 }, |
| { type: ControlPointType.BOTTOM_LEFT, left: 0, top: 100 }, |
| { type: ControlPointType.BOTTOM_CENTER, left: 50, top: 100 }, |
| { type: ControlPointType.BOTTOM_RIGHT, left: 100, top: 100 } |
| ]; |
| |
| positions.forEach(pos => { |
| const point = document.createElement('div'); |
| point.className = 'control-point'; |
| point.dataset.type = pos.type; |
| point.style.display = 'none'; |
| |
| |
| point.style.left = `calc(${pos.left}% - 5px)`; |
| point.style.top = `calc(${pos.top}% - 5px)`; |
| |
| |
| point.addEventListener('mousedown', startControlPointDrag); |
| |
| previewImage.parentNode.appendChild(point); |
| }); |
| } |
| |
| |
| function updateImageStyle(x, y, width, height, rotation) { |
| previewImage.style.left = `${x}px`; |
| previewImage.style.top = `${y}px`; |
| previewImage.style.width = `${width}px`; |
| previewImage.style.height = `${height}px`; |
| previewImage.style.transform = `rotate(${rotation}deg) scaleX(${scaleX}) scaleY(${scaleY})`; |
| |
| |
| const rotateX = x + width / 2 - 15; |
| const rotateY = y - 40; |
| rotateHandle.style.left = `${rotateX}px`; |
| rotateHandle.style.top = `${rotateY}px`; |
| |
| |
| updateControlPointsPosition(x, y, width, height, rotation); |
| } |
| |
| |
| function updateControlPointsPosition(x, y, width, height, rotation) { |
| const points = document.querySelectorAll('.control-point'); |
| const centerX = x + width / 2; |
| const centerY = y + height / 2; |
| |
| points.forEach(point => { |
| const type = point.dataset.type; |
| let pointX, pointY; |
| |
| switch(type) { |
| case ControlPointType.TOP_LEFT: |
| pointX = x; |
| pointY = y; |
| break; |
| case ControlPointType.TOP_CENTER: |
| pointX = centerX - 5; |
| pointY = y; |
| break; |
| case ControlPointType.TOP_RIGHT: |
| pointX = x + width - 10; |
| pointY = y; |
| break; |
| case ControlPointType.MIDDLE_LEFT: |
| pointX = x; |
| pointY = centerY - 5; |
| break; |
| case ControlPointType.MIDDLE_RIGHT: |
| pointX = x + width - 10; |
| pointY = centerY - 5; |
| break; |
| case ControlPointType.BOTTOM_LEFT: |
| pointX = x; |
| pointY = y + height - 10; |
| break; |
| case ControlPointType.BOTTOM_CENTER: |
| pointX = centerX - 5; |
| pointY = y + height - 10; |
| break; |
| case ControlPointType.BOTTOM_RIGHT: |
| pointX = x + width - 10; |
| pointY = y + height - 10; |
| break; |
| } |
| |
| |
| if (rotation !== 0) { |
| const rad = rotation * Math.PI / 180; |
| const rotatedX = centerX + (pointX - centerX) * Math.cos(rad) - (pointY - centerY) * Math.sin(rad); |
| const rotatedY = centerY + (pointX - centerX) * Math.sin(rad) + (pointY - centerY) * Math.cos(rad); |
| pointX = rotatedX; |
| pointY = rotatedY; |
| } |
| |
| point.style.left = `${pointX}px`; |
| point.style.top = `${pointY}px`; |
| }); |
| } |
| |
| |
| function updateFormInputs(x, y, width, height, rotation) { |
| posXInput.value = Math.round(x); |
| posYInput.value = Math.round(y); |
| widthInput.value = Math.round(width); |
| heightInput.value = Math.round(height); |
| rotationInput.value = Math.round(rotation); |
| } |
| |
| |
| previewImage.addEventListener('mousedown', function(e) { |
| if (!isFullscreen && e.target === previewImage) { |
| isDragging = true; |
| startX = e.clientX; |
| startY = e.clientY; |
| startImageX = parseFloat(previewImage.style.left || '0'); |
| startImageY = parseFloat(previewImage.style.top || '0'); |
| e.preventDefault(); |
| } |
| }); |
| |
| |
| function startControlPointDrag(e) { |
| if (!isFullscreen) { |
| activeControlPoint = e.target.dataset.type; |
| startX = e.clientX; |
| startY = e.clientY; |
| startWidth = parseFloat(previewImage.style.width); |
| startHeight = parseFloat(previewImage.style.height); |
| startImageX = parseFloat(previewImage.style.left || '0'); |
| startImageY = parseFloat(previewImage.style.top || '0'); |
| e.preventDefault(); |
| e.stopPropagation(); |
| } |
| } |
| |
| |
| rotateHandle.addEventListener('mousedown', function(e) { |
| if (!isFullscreen) { |
| isRotating = true; |
| startX = e.clientX; |
| startY = e.clientY; |
| startRotation = parseFloat(rotationInput.value) || 0; |
| e.preventDefault(); |
| } |
| }); |
| |
| |
| panelHeader.addEventListener('mousedown', function(e) { |
| if (!isFullscreen && (e.target === panelHeader || e.target.classList.contains('panel-title'))) { |
| isDraggingPanel = true; |
| startX = e.clientX; |
| startY = e.clientY; |
| startPanelX = settingsPanel.offsetLeft; |
| startPanelY = settingsPanel.offsetTop; |
| e.preventDefault(); |
| } |
| }); |
| |
| |
| togglePanel.addEventListener('click', function() { |
| if (!isFullscreen) { |
| settingsPanel.classList.toggle('panel-collapsed'); |
| togglePanel.textContent = settingsPanel.classList.contains('panel-collapsed') ? '+' : '−'; |
| } |
| }); |
| |
| |
| document.addEventListener('mousemove', function(e) { |
| if (isFullscreen) return; |
| |
| if (isDragging) { |
| const dx = e.clientX - startX; |
| const dy = e.clientY - startY; |
| const newX = startImageX + dx; |
| const newY = startImageY + dy; |
| |
| const currentWidth = parseFloat(previewImage.style.width); |
| const currentHeight = parseFloat(previewImage.style.height); |
| const rotation = parseFloat(rotationInput.value) || 0; |
| |
| updateImageStyle(newX, newY, currentWidth, currentHeight, rotation); |
| updateFormInputs(newX, newY, currentWidth, currentHeight, rotation); |
| } else if (activeControlPoint) { |
| const dx = e.clientX - startX; |
| const dy = e.clientY - startY; |
| let newWidth = startWidth; |
| let newHeight = startHeight; |
| let newX = startImageX; |
| let newY = startImageY; |
| const rotation = parseFloat(rotationInput.value) || 0; |
| |
| switch(activeControlPoint) { |
| case ControlPointType.TOP_LEFT: |
| newWidth = startWidth - dx; |
| newHeight = newWidth / aspectRatio; |
| newX = startImageX + dx; |
| newY = startImageY + (startHeight - newHeight); |
| break; |
| case ControlPointType.TOP_CENTER: |
| newHeight = startHeight - dy; |
| newY = startImageY + dy; |
| break; |
| case ControlPointType.TOP_RIGHT: |
| newWidth = startWidth + dx; |
| newHeight = newWidth / aspectRatio; |
| newY = startImageY + (startHeight - newHeight); |
| break; |
| case ControlPointType.MIDDLE_LEFT: |
| newWidth = startWidth - dx; |
| newX = startImageX + dx; |
| break; |
| case ControlPointType.MIDDLE_RIGHT: |
| newWidth = startWidth + dx; |
| break; |
| case ControlPointType.BOTTOM_LEFT: |
| newWidth = startWidth - dx; |
| newHeight = newWidth / aspectRatio; |
| newX = startImageX + dx; |
| break; |
| case ControlPointType.BOTTOM_CENTER: |
| newHeight = startHeight + dy; |
| break; |
| case ControlPointType.BOTTOM_RIGHT: |
| newWidth = startWidth + dx; |
| newHeight = newWidth / aspectRatio; |
| break; |
| } |
| |
| |
| newWidth = Math.max(10, newWidth); |
| newHeight = Math.max(10, newHeight); |
| |
| updateImageStyle(newX, newY, newWidth, newHeight, rotation); |
| updateFormInputs(newX, newY, newWidth, newHeight, rotation); |
| } else if (isRotating) { |
| const centerX = parseFloat(previewImage.style.left) + parseFloat(previewImage.style.width) / 2; |
| const centerY = parseFloat(previewImage.style.top) + parseFloat(previewImage.style.height) / 2; |
| |
| const angle = Math.atan2(e.clientY - centerY, e.clientX - centerX) * 180 / Math.PI; |
| const newRotation = (angle + 90) % 360; |
| |
| const currentX = parseFloat(previewImage.style.left); |
| const currentY = parseFloat(previewImage.style.top); |
| const currentWidth = parseFloat(previewImage.style.width); |
| const currentHeight = parseFloat(previewImage.style.height); |
| |
| updateImageStyle(currentX, currentY, currentWidth, currentHeight, newRotation); |
| updateFormInputs(currentX, currentY, currentWidth, currentHeight, newRotation); |
| } else if (isDraggingPanel) { |
| const dx = e.clientX - startX; |
| const dy = e.clientY - startY; |
| |
| let newLeft = startPanelX + dx; |
| let newTop = startPanelY + dy; |
| |
| |
| newLeft = Math.max(0, Math.min(window.innerWidth - settingsPanel.offsetWidth, newLeft)); |
| newTop = Math.max(0, Math.min(window.innerHeight - panelHeader.offsetHeight, newTop)); |
| |
| settingsPanel.style.left = `${newLeft}px`; |
| settingsPanel.style.top = `${newTop}px`; |
| } |
| }); |
| |
| |
| document.addEventListener('mouseup', function() { |
| isDragging = false; |
| activeControlPoint = null; |
| isRotating = false; |
| isDraggingPanel = false; |
| }); |
| |
| |
| posXInput.addEventListener('input', updateFromForm); |
| posYInput.addEventListener('input', updateFromForm); |
| widthInput.addEventListener('input', updateFromForm); |
| heightInput.addEventListener('input', updateFromForm); |
| rotationInput.addEventListener('input', updateFromForm); |
| |
| function updateFromForm() { |
| const x = parseFloat(posXInput.value) || 0; |
| const y = parseFloat(posYInput.value) || 0; |
| const width = parseFloat(widthInput.value) || 100; |
| const height = parseFloat(heightInput.value) || 100; |
| const rotation = parseFloat(rotationInput.value) || 0; |
| |
| updateImageStyle(x, y, width, height, rotation); |
| } |
| |
| |
| flipHorizontalBtn.addEventListener('click', function() { |
| scaleX *= -1; |
| updateImageStyle( |
| parseFloat(previewImage.style.left), |
| parseFloat(previewImage.style.top), |
| parseFloat(previewImage.style.width), |
| parseFloat(previewImage.style.height), |
| parseFloat(rotationInput.value) || 0 |
| ); |
| }); |
| |
| flipVerticalBtn.addEventListener('click', function() { |
| scaleY *= -1; |
| updateImageStyle( |
| parseFloat(previewImage.style.left), |
| parseFloat(previewImage.style.top), |
| parseFloat(previewImage.style.width), |
| parseFloat(previewImage.style.height), |
| parseFloat(rotationInput.value) || 0 |
| ); |
| }); |
| |
| |
| okButton.addEventListener('click', toggleFullscreen); |
| |
| function toggleFullscreen() { |
| if (!previewImage.src) return; |
| |
| if (isFullscreen) { |
| exitFullscreen(); |
| } else { |
| enterFullscreen(); |
| } |
| } |
| |
| function enterFullscreen() { |
| isFullscreen = true; |
| (document.documentElement.requestFullscreen || document.documentElement.webkitRequestFullscreen || document.documentElement.mozRequestFullScreen || document.documentElement.msRequestFullscreen).call(document.documentElement); |
| |
| document.querySelectorAll('.control-point').forEach(point => { |
| point.classList.add('hidden'); |
| }); |
| rotateHandle.classList.add('hidden'); |
| settingsPanel.classList.add('hidden'); |
| |
| |
| previewContainer.classList.add('fullscreen-mode'); |
| |
| |
| previewImage.style.cursor = 'default'; |
| |
| |
| const closeButton = document.createElement('div'); |
| closeButton.className = 'close-fullscreen'; |
| closeButton.innerHTML = '×'; |
| closeButton.addEventListener('click', exitFullscreen); |
| document.body.appendChild(closeButton); |
| |
| |
| document.addEventListener('keydown', handleFullscreenKeydown); |
| |
| |
| document.body.style.cursor = 'none'; |
| } |
| |
| function exitFullscreen() { |
| isFullscreen = false; |
| (document.exitFullscreen || document.webkitExitFullscreen || document.mozCancelFullScreen || document.msExitFullscreen).call(document); |
| |
| |
| document.querySelectorAll('.control-point').forEach(point => { |
| point.classList.remove('hidden'); |
| }); |
| rotateHandle.classList.remove('hidden'); |
| settingsPanel.classList.remove('hidden'); |
| |
| |
| previewContainer.classList.remove('fullscreen-mode'); |
| |
| |
| previewImage.style.cursor = 'move'; |
| |
| |
| const closeButton = document.querySelector('.close-fullscreen'); |
| if (closeButton) closeButton.remove(); |
| |
| |
| document.body.style.cursor = ''; |
| |
| |
| document.removeEventListener('keydown', handleFullscreenKeydown); |
| } |
| |
| function handleFullscreenKeydown(e) { |
| if (e.key === 'Escape') { |
| exitFullscreen(); |
| } |
| } |
| }); |
| </script> |
| </body> |
| </html> |