Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <title>Coordinate Maker</title> | |
| <meta name="viewport" content="width=800,user-scalable=no"> | |
| <style> | |
| /* ...(CSSは変更不要なので省略。前回そのまま)... */ | |
| #img-container { | |
| position: relative; | |
| width: 800px; | |
| height: 600px; | |
| border: 2px dashed #aaa; | |
| background: #f9f9f9; | |
| user-select: none; | |
| display: inline-block; | |
| overflow: hidden; | |
| transition: border-color 0.2s, background 0.2s; | |
| } | |
| #img-container.dragover { | |
| border-color: #2a7; | |
| background: #e2ffe9; | |
| } | |
| #the-img { | |
| width: 100%; | |
| height: 100%; | |
| object-fit: contain; | |
| user-select: none; | |
| pointer-events: none; | |
| display: none; | |
| } | |
| .rect { | |
| position: absolute; | |
| border: 2px solid #2a7; | |
| background: rgba(44,222,88,0.15); | |
| box-sizing: border-box; | |
| cursor: move; | |
| pointer-events: auto; | |
| transition: box-shadow 0.2s; | |
| } | |
| .rect.selected { | |
| border: 2px solid #f00; | |
| background: rgba(255,100,100,0.15); | |
| z-index: 10; | |
| box-shadow: 0 0 10px #f00a; | |
| } | |
| .handle { | |
| position: absolute; | |
| width: 14px; height: 14px; | |
| margin: -7px 0 0 -7px; | |
| background: #2a7; | |
| border-radius: 50%; | |
| cursor: pointer; | |
| z-index: 20; | |
| border: 2px solid #fff; | |
| box-shadow: 0 0 2px #0004; | |
| display: block; | |
| } | |
| .rect.selected .handle { | |
| background: #f00; | |
| } | |
| .rotate-handle, .quad-rotate-handle { | |
| position: absolute; | |
| width: 18px; | |
| height: 18px; | |
| background: #ff0; | |
| border: 2px solid #f90; | |
| border-radius: 50%; | |
| cursor: grab; | |
| z-index: 30; | |
| display: block; | |
| box-shadow: 0 0 4px #0006; | |
| } | |
| .rotate-handle { | |
| left: 50%; | |
| top: -32px; | |
| transform: translate(-50%,0); | |
| } | |
| .quad-svg { | |
| position: absolute; | |
| left: 0; top: 0; | |
| width: 100%; height: 100%; | |
| pointer-events: none; | |
| z-index: 2; | |
| } | |
| .quad-svg polygon { | |
| fill: rgba(44,222,88,0.15); | |
| stroke: #f00; | |
| stroke-width: 2; | |
| pointer-events: stroke; | |
| } | |
| #coords { | |
| font-family: monospace; | |
| margin-top: 10px; | |
| background: #f8f8f8; | |
| padding: 10px; | |
| border: 1px solid #bbb; | |
| width: 480px; | |
| min-height: 120px; | |
| } | |
| #drop-message { | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| width: 90%; | |
| transform: translate(-50%, -50%); | |
| color: #2a7; | |
| font-size: 1.6em; | |
| text-align: center; | |
| pointer-events: none; | |
| display: none; | |
| background: rgba(255,255,255,0.75); | |
| border-radius: 10px; | |
| padding: 0.5em 0; | |
| z-index: 1000; | |
| } | |
| #toolbar { | |
| margin-bottom: 0.5em; | |
| display: flex; | |
| align-items: center; | |
| flex-wrap: wrap; | |
| gap: 0.5em; | |
| } | |
| #toolbar button, #toolbar input[type="file"] { | |
| margin-right: 1em; | |
| padding: 0.5em 1em; | |
| font-size: 1em; | |
| cursor: pointer; | |
| } | |
| #filename-input { | |
| padding: 0.5em 1em; | |
| font-size: 1em; | |
| width: 220px; | |
| margin-right: 1em; | |
| } | |
| #file-name-label { | |
| font-size: 0.98em; | |
| margin-right: 1em; | |
| color: #333; | |
| min-width: 120px; | |
| max-width: 220px; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| display: inline-block; | |
| vertical-align: middle; | |
| } | |
| #memory-list { | |
| margin-top: 1em; | |
| font-family: monospace; | |
| font-size: 1em; | |
| background: #fff; | |
| border: 1px solid #bbb; | |
| padding: 8px 12px; | |
| width: 480px; | |
| min-height: 40px; | |
| } | |
| .memory-item { | |
| margin-bottom: 0.5em; | |
| border-bottom: 1px dotted #ccc; | |
| padding-bottom: 0.3em; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| } | |
| .memory-item:last-child { | |
| border-bottom: none; | |
| } | |
| .memory-item .memory-label { | |
| display: inline-block; | |
| max-width: 360px; | |
| word-break: break-all; | |
| } | |
| .memory-item .memory-delete-btn { | |
| color: #c00; | |
| background: transparent; | |
| border: none; | |
| cursor: pointer; | |
| margin-left: 1em; | |
| font-size: 1.1em; | |
| } | |
| #add-rect-btn, #add-quad-btn, #save-rect-btn, #memory-save-btn { | |
| margin-right: 1em; | |
| padding: 0.5em 1em; | |
| font-size: 1em; | |
| cursor: pointer; | |
| } | |
| #save-rect-btn:disabled { | |
| color: #ccc; | |
| border-color: #ccc; | |
| cursor: default; | |
| } | |
| input[type="file"]::-webkit-file-upload-button { visibility: visible; } | |
| input[type="file"]::file-selector-button { visibility: visible; } | |
| input[type="file"]::-ms-value { display: none; } | |
| input[type="file"]::-webkit-file-upload-button, | |
| input[type="file"]::before { | |
| content: none ; | |
| } | |
| input[type="file"] { | |
| color: transparent; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <h2>Coordinate Maker</h2> | |
| <div id="toolbar"> | |
| <label style="display:inline-block; position:relative;"> | |
| <input type="file" id="img-input" accept="image/*" style="width:140px;"> | |
| <span style="position:absolute;left:12px;top:8px;pointer-events:none;color:#555;font-size:1em;" id="file-input-label">Choose File</span> | |
| </label> | |
| <span id="file-name-label"></span> | |
| <button id="add-rect-btn">Rectangle Mode</button> | |
| <button id="add-quad-btn">Free Transform Mode</button> | |
| <button id="save-rect-btn" disabled>Save Rectangle</button> | |
| <input type="text" id="filename-input" placeholder="templates.json"> | |
| <button id="memory-save-btn">Save Memory to File</button> | |
| </div> | |
| <div id="img-container"> | |
| <span id="drop-message">Drop image here</span> | |
| <img id="the-img"> | |
| </div> | |
| <div id="coords">Coordinates:<br></div> | |
| <div id="memory-list"></div> | |
| <script> | |
| // === DOM取得 === | |
| const imgInput = document.getElementById('img-input'); | |
| const img = document.getElementById('the-img'); | |
| const container = document.getElementById('img-container'); | |
| const coords = document.getElementById('coords'); | |
| const dropMessage = document.getElementById('drop-message'); | |
| const addRectBtn = document.getElementById('add-rect-btn'); | |
| const addQuadBtn = document.getElementById('add-quad-btn'); | |
| const saveRectBtn = document.getElementById('save-rect-btn'); | |
| const memorySaveBtn = document.getElementById('memory-save-btn'); | |
| const memoryList = document.getElementById('memory-list'); | |
| const filenameInput = document.getElementById('filename-input'); | |
| const fileNameLabel = document.getElementById('file-name-label'); | |
| const fileInputLabel = document.getElementById('file-input-label'); | |
| // --- State --- | |
| let imgLoaded = false, imgNaturalWidth = 0, imgNaturalHeight = 0; | |
| let dispImgInfo = {left:0, top:0, width:0, height:0}; | |
| let currentImageName = ''; | |
| let currentMode = "rect"; // "rect" or "quad" | |
| let rectObj = null; | |
| let drawing = false, moving = false, resizing = false, rotating = false; | |
| let startX = 0, startY = 0, offsetX = 0, offsetY = 0, originX = 0, originY = 0, rotateStartAngle = 0; | |
| let activeHandle = null, memory = []; | |
| let quadPoints = null; // only for quad mode | |
| let quadSVG = null; | |
| let rotateHandle = null; | |
| let quadRotateHandle = null; | |
| // --- File input label (hide when file is selected) --- | |
| imgInput.addEventListener('change', (e) => { | |
| if(imgInput.files && imgInput.files.length) fileInputLabel.style.display = 'none'; | |
| else fileInputLabel.style.display = ''; | |
| }); | |
| imgInput.addEventListener('input', (e) => { | |
| if(imgInput.files && imgInput.files.length) fileInputLabel.style.display = 'none'; | |
| else fileInputLabel.style.display = ''; | |
| }); | |
| // --- Mode Buttons --- | |
| addRectBtn.onclick = () => { | |
| currentMode = "rect"; | |
| addRectBtn.style.background = "#baffba"; | |
| addQuadBtn.style.background = ""; | |
| clearRect(); saveRectBtn.disabled = true; | |
| }; | |
| addQuadBtn.onclick = () => { | |
| currentMode = "quad"; | |
| addQuadBtn.style.background = "#baffba"; | |
| addRectBtn.style.background = ""; | |
| clearRect(); saveRectBtn.disabled = true; | |
| }; | |
| addRectBtn.style.background = "#baffba"; | |
| function updateFileNameLabel(name) { fileNameLabel.textContent = name ? name : ""; } | |
| function loadImageFromFile(file) { | |
| if (!file || !file.type.match(/^image\//)) return; | |
| const reader = new FileReader(); | |
| reader.onload = ev => { img.src = ev.target.result; currentImageName = file.name || ''; updateFileNameLabel(currentImageName); }; | |
| reader.readAsDataURL(file); | |
| } | |
| imgInput.addEventListener('change', e => { | |
| const file = e.target.files[0]; | |
| imgInput.value = ""; | |
| updateFileNameLabel(""); | |
| if (file) loadImageFromFile(file); | |
| }); | |
| container.addEventListener('dragover', e => { e.preventDefault(); e.stopPropagation(); container.classList.add('dragover'); dropMessage.textContent = "Drop image here"; dropMessage.style.display = 'block'; }); | |
| container.addEventListener('dragleave', e => { e.preventDefault(); e.stopPropagation(); container.classList.remove('dragover'); dropMessage.style.display = 'none'; }); | |
| container.addEventListener('drop', e => { | |
| e.preventDefault(); e.stopPropagation(); | |
| container.classList.remove('dragover'); dropMessage.style.display = 'none'; | |
| if (e.dataTransfer.files && e.dataTransfer.files[0]) { const file = e.dataTransfer.files[0]; updateFileNameLabel(""); loadImageFromFile(file); } | |
| }); | |
| img.addEventListener('load', () => { | |
| imgLoaded = true; img.style.display = "block"; | |
| imgNaturalWidth = img.naturalWidth; imgNaturalHeight = img.naturalHeight; | |
| img.style.width = '100%'; img.style.height = '100%'; img.style.objectFit = 'contain'; | |
| clearRect(); updateDispImgInfo(); updateCoords(); | |
| }); | |
| function updateDispImgInfo() { | |
| const cW = container.clientWidth, cH = container.clientHeight, iW = imgNaturalWidth, iH = imgNaturalHeight; | |
| let dispW = cW, dispH = cH, dispL = 0, dispT = 0, imgAspect = iW / iH, contAspect = cW / cH; | |
| if (imgAspect > contAspect) { dispW = cW; dispH = cW / imgAspect; dispT = (cH - dispH) / 2; dispL = 0; } | |
| else { dispH = cH; dispW = cH * imgAspect; dispL = (cW - dispW) / 2; dispT = 0; } | |
| dispImgInfo = {left:dispL, top:dispT, width:dispW, height:dispH}; | |
| } | |
| window.addEventListener('resize', updateDispImgInfo); | |
| // --- Drawing --- | |
| container.addEventListener('mousedown', e => { | |
| if (!imgLoaded || drawing || rectObj) return; | |
| if (e.target !== container && e.target !== img) return; | |
| const rectC = container.getBoundingClientRect(); | |
| startX = e.clientX - rectC.left; startY = e.clientY - rectC.top; | |
| let el = document.createElement('div'); | |
| el.className = 'rect selected'; | |
| container.appendChild(el); | |
| rectObj = { | |
| type: currentMode, | |
| el: el, | |
| x: startX, y: startY, w: 0, h: 0, | |
| angle: 0, handles: [] | |
| }; | |
| setSelectedRect(true); updateRectUI(rectObj); | |
| drawing = true; saveRectBtn.disabled = true; | |
| function onMove(ev) { | |
| const currX = ev.clientX - rectC.left, currY = ev.clientY - rectC.top; | |
| rectObj.x = Math.min(startX, currX); rectObj.y = Math.min(startY, currY); | |
| rectObj.w = Math.abs(currX - startX); rectObj.h = Math.abs(currY - startY); | |
| updateRectUI(rectObj); updateCoords(rectObj); | |
| } | |
| function onUp(ev) { | |
| drawing = false; document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); | |
| if (rectObj.w < 10 || rectObj.h < 10) { clearRect(); saveRectBtn.disabled = true; } | |
| else { | |
| createHandles(rectObj); | |
| if(currentMode==="rect") createRotateHandle(rectObj); | |
| else { quadPoints = null; createHandles(rectObj); } | |
| updateCoords(rectObj); saveRectBtn.disabled = false; | |
| } | |
| } | |
| document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); | |
| e.preventDefault(); | |
| }); | |
| // --- Rect/Quad move --- | |
| container.addEventListener('mousedown', e => { | |
| if (drawing || !imgLoaded) return; | |
| if (!rectObj || !rectObj.el) return; | |
| if (e.target === rectObj.el && !rotating) { | |
| setSelectedRect(true); saveRectBtn.disabled = false; moving = true; | |
| const rectC = container.getBoundingClientRect(); | |
| offsetX = e.clientX - rectC.left - rectObj.x; | |
| offsetY = e.clientY - rectC.top - rectObj.y; | |
| document.body.style.cursor = "move"; | |
| function onMove(ev) { | |
| let newX = ev.clientX - rectC.left - offsetX, newY = ev.clientY - rectC.top - offsetY; | |
| if(currentMode==="quad" && quadPoints) { | |
| let dx = newX - rectObj.x, dy = newY - rectObj.y; | |
| for(let i=0;i<4;i++) { | |
| quadPoints[i][0] += dx; | |
| quadPoints[i][1] += dy; | |
| } | |
| rectObj.x = newX; rectObj.y = newY; | |
| updateQuadSVG(); | |
| updateHandlesQuad(); | |
| updateCoordsQuad(); | |
| updateQuadRotateHandle(); | |
| } else { | |
| rectObj.x = newX; rectObj.y = newY; updateRectUI(rectObj); updateHandles(rectObj); updateRotateHandle(rectObj); updateCoords(rectObj); | |
| } | |
| } | |
| function onUp(ev) { | |
| moving = false; document.body.style.cursor = ""; document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); | |
| } | |
| document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); e.stopPropagation(); | |
| } | |
| }); | |
| // --- Handles (resize/drag) --- | |
| function createHandles(rectObj) { | |
| if (rectObj.handles && rectObj.handles.length) rectObj.handles.forEach(h=>h.remove()); | |
| rectObj.handles = []; | |
| const positions = [ | |
| {name:"tl", left:0, top:0}, | |
| {name:"tr", left:1, top:0}, | |
| {name:"br", left:1, top:1}, | |
| {name:"bl", left:0, top:1}, | |
| ]; | |
| for(let i=0; i<4; i++) { | |
| let h = document.createElement('div'); | |
| h.className = 'handle'; | |
| h.dataset.handle = positions[i].name; | |
| h.style.left = (positions[i].left*100) + '%'; | |
| h.style.top = (positions[i].top*100) + '%'; | |
| h.style.cursor = ["nwse-resize","nesw-resize","nwse-resize","nesw-resize"][i]; | |
| h.addEventListener('mousedown', evt => { | |
| resizing = true; activeHandle = i; | |
| originX = evt.clientX; originY = evt.clientY; | |
| let orig = {...rectObj}; | |
| let origAngle = rectObj.angle; | |
| let origPoints = getRectCornerPoints(rectObj); | |
| document.body.style.cursor = h.style.cursor; | |
| function onMove(ev) { | |
| if(rectObj.type==="rect") { | |
| // 対角固定のリサイズ | |
| const idx = activeHandle, oppIdx = (idx+2)%4; | |
| const [fx,fy] = origPoints[oppIdx]; | |
| let mx = ev.clientX, my = ev.clientY; | |
| let [cx,cy] = getRectCenter(orig); | |
| let rad = -origAngle * Math.PI/180; | |
| let tx = mx - container.getBoundingClientRect().left, ty = my - container.getBoundingClientRect().top; | |
| let dx = (tx-cx)*Math.cos(rad)+(ty-cy)*Math.sin(rad); | |
| let dy =-(tx-cx)*Math.sin(rad)+(ty-cy)*Math.cos(rad); | |
| let ofx = fx - cx, ofy = fy - cy; | |
| let newW = Math.abs(dx-ofx), newH = Math.abs(dy-ofy); | |
| let newCx = (dx+ofx)/2+cx, newCy = (dy+ofy)/2+cy; | |
| rectObj.w = Math.max(10,newW); rectObj.h = Math.max(10,newH); | |
| rectObj.x = newCx - rectObj.w/2; rectObj.y = newCy - rectObj.h/2; | |
| updateRectUI(rectObj); updateHandles(rectObj); updateRotateHandle(rectObj); updateCoords(rectObj); | |
| } else { | |
| // --- quad: 他の3点は絶対固定 --- | |
| if(!quadPoints) { | |
| quadPoints = getRectCornerPoints(rectObj).map(p=>[...p]); | |
| showQuadSVG(); | |
| createQuadRotateHandle(); | |
| } | |
| const idx = activeHandle; | |
| const tx = ev.clientX - container.getBoundingClientRect().left; | |
| const ty = ev.clientY - container.getBoundingClientRect().top; | |
| quadPoints[idx][0] = tx; | |
| quadPoints[idx][1] = ty; | |
| updateQuadSVG(); | |
| updateHandlesQuad(); | |
| updateCoordsQuad(); | |
| updateQuadRotateHandle(); | |
| } | |
| } | |
| function onUp(ev) { | |
| resizing = false; document.body.style.cursor = ""; document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); | |
| } | |
| document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); | |
| evt.stopPropagation(); | |
| }); | |
| rectObj.el.appendChild(h); rectObj.handles.push(h); | |
| } | |
| if(currentMode==="rect" || !quadPoints) updateHandles(rectObj); | |
| else updateHandlesQuad(); | |
| } | |
| function updateHandles(rectObj) { | |
| let pts = getRectCornerPoints(rectObj); | |
| for(let i=0;i<4;i++) { | |
| rectObj.handles[i].style.left = pts[i][0]-rectObj.x+'px'; | |
| rectObj.handles[i].style.top = pts[i][1]-rectObj.y+'px'; | |
| } | |
| } | |
| function updateHandlesQuad() { | |
| if(!rectObj || !rectObj.handles || !quadPoints) return; | |
| for(let i=0;i<4;i++) { | |
| rectObj.handles[i].style.left = quadPoints[i][0] - rectObj.x + 'px'; | |
| rectObj.handles[i].style.top = quadPoints[i][1] - rectObj.y + 'px'; | |
| } | |
| } | |
| function getRectCenter(r) { | |
| return [r.x + r.w/2, r.y + r.h/2]; | |
| } | |
| function getRectCornerPoints(r) { | |
| let cx = r.x + r.w/2, cy = r.y + r.h/2; | |
| let rad = (r.angle||0)*Math.PI/180; | |
| let dx = r.w/2, dy = r.h/2; | |
| let corners = [ | |
| [-dx,-dy], | |
| [ dx,-dy], | |
| [ dx, dy], | |
| [-dx, dy] | |
| ]; | |
| return corners.map(([ox,oy])=>{ | |
| let x = ox*Math.cos(rad)-oy*Math.sin(rad)+cx; | |
| let y = ox*Math.sin(rad)+oy*Math.cos(rad)+cy; | |
| return [x,y]; | |
| }); | |
| } | |
| // --- 回転ハンドルと回転処理 --- | |
| function createRotateHandle(rectObj) { | |
| if(rotateHandle) rotateHandle.remove(); | |
| rotateHandle = document.createElement('div'); | |
| rotateHandle.className = 'rotate-handle'; | |
| rectObj.el.appendChild(rotateHandle); | |
| updateRotateHandle(rectObj); | |
| rotateHandle.addEventListener('mousedown', function(e) { | |
| rotating = true; | |
| let [cx,cy] = getRectCenter(rectObj); | |
| const rectC = container.getBoundingClientRect(); | |
| let mx = e.clientX - rectC.left, my = e.clientY - rectC.top; | |
| let startAngle = rectObj.angle || 0; | |
| let baseAngle = Math.atan2(my - cx, mx - cy); | |
| document.body.style.cursor = "crosshair"; | |
| function onMove(ev) { | |
| let mx2 = ev.clientX - rectC.left, my2 = ev.clientY - rectC.top; | |
| let currAngle = Math.atan2(my2 - cx, mx2 - cy); | |
| let angle = startAngle + (currAngle - baseAngle) * 180 / Math.PI; | |
| rectObj.angle = angle; | |
| updateRectUI(rectObj); updateHandles(rectObj); updateRotateHandle(rectObj); updateCoords(rectObj); | |
| } | |
| function onUp(ev) { | |
| rotating = false; document.body.style.cursor = ""; document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); | |
| } | |
| document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); | |
| e.stopPropagation(); | |
| }); | |
| } | |
| function updateRotateHandle(rectObj) { | |
| rotateHandle.style.left = '50%'; | |
| rotateHandle.style.top = '-32px'; | |
| rotateHandle.style.transform = `translate(-50%,0)`; | |
| rotateHandle.style.display = currentMode === "rect" ? "block" : "none"; | |
| } | |
| // Quad専用の回転ハンドル | |
| function createQuadRotateHandle() { | |
| if(quadRotateHandle) quadRotateHandle.remove(); | |
| quadRotateHandle = document.createElement('div'); | |
| quadRotateHandle.className = 'quad-rotate-handle rotate-handle'; | |
| quadRotateHandle.style.position = 'absolute'; | |
| updateQuadRotateHandle(); | |
| container.appendChild(quadRotateHandle); | |
| quadRotateHandle.addEventListener('mousedown', function(e) { | |
| rotating = true; | |
| let center = getQuadCenter(quadPoints); | |
| let rectC = container.getBoundingClientRect(); | |
| let mx = e.clientX - rectC.left, my = e.clientY - rectC.top; | |
| let baseAngle = Math.atan2(my - center[1], mx - center[0]); | |
| document.body.style.cursor = "crosshair"; | |
| function onMove(ev) { | |
| let mx2 = ev.clientX - rectC.left, my2 = ev.clientY - rectC.top; | |
| let currAngle = Math.atan2(my2 - center[1], mx2 - center[0]); | |
| let diff = currAngle - baseAngle; | |
| let cos = Math.cos(diff), sin = Math.sin(diff); | |
| for(let i=0;i<4;i++) { | |
| let x = quadPoints[i][0] - center[0], y = quadPoints[i][1] - center[1]; | |
| let rx = x * cos - y * sin; | |
| let ry = x * sin + y * cos; | |
| quadPoints[i][0] = rx + center[0]; | |
| quadPoints[i][1] = ry + center[1]; | |
| } | |
| baseAngle = currAngle; | |
| updateQuadSVG(); updateHandlesQuad(); updateCoordsQuad(); updateQuadRotateHandle(); | |
| } | |
| function onUp(ev) { | |
| rotating = false; document.body.style.cursor = ""; document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); | |
| } | |
| document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); | |
| e.stopPropagation(); | |
| }); | |
| } | |
| function updateQuadRotateHandle() { | |
| if(!quadPoints || !quadRotateHandle) return; | |
| let center = getQuadCenter(quadPoints); | |
| let p0 = quadPoints[0], p1 = quadPoints[1]; | |
| let dx = p1[0] - p0[0], dy = p1[1] - p0[1]; | |
| let mx = (p0[0]+p1[0])/2, my = (p0[1]+p1[1])/2; | |
| let len = Math.sqrt(dx*dx+dy*dy); | |
| let nx = -dy/len, ny = dx/len; // normal | |
| let hx = mx + nx*40, hy = my + ny*40; | |
| quadRotateHandle.style.left = (hx) + 'px'; | |
| quadRotateHandle.style.top = (hy) + 'px'; | |
| quadRotateHandle.style.transform = 'translate(-50%,-50%)'; | |
| quadRotateHandle.style.display = currentMode === "quad" && quadPoints ? "block" : "none"; | |
| } | |
| function getQuadCenter(pts) { | |
| let x=0, y=0; | |
| for(let i=0;i<4;i++) { x+=pts[i][0]; y+=pts[i][1]; } | |
| return [x/4, y/4]; | |
| } | |
| function updateRectUI(rectObj) { | |
| rectObj.el.style.left = rectObj.x + 'px'; | |
| rectObj.el.style.top = rectObj.y + 'px'; | |
| rectObj.el.style.width = rectObj.w + 'px'; | |
| rectObj.el.style.height = rectObj.h + 'px'; | |
| rectObj.el.style.transform = `rotate(${rectObj.angle||0}deg)`; | |
| rectObj.el.style.transformOrigin = "50% 50%"; | |
| if(currentMode==="rect") { | |
| if(rotateHandle) rotateHandle.style.display = "block"; | |
| if(quadRotateHandle) quadRotateHandle.style.display = "none"; | |
| } | |
| if(currentMode==="quad" && quadPoints) { | |
| updateQuadSVG(); | |
| updateHandlesQuad(); | |
| updateQuadRotateHandle(); | |
| if(rotateHandle) rotateHandle.style.display = "none"; | |
| if(quadRotateHandle) quadRotateHandle.style.display = "block"; | |
| } | |
| } | |
| function setSelectedRect(selected) { | |
| if (!rectObj) return; | |
| if (selected) rectObj.el.classList.add('selected'); | |
| else rectObj.el.classList.remove('selected'); | |
| saveRectBtn.disabled = !rectObj; | |
| } | |
| function clearRect() { | |
| if(rectObj && rectObj.el) rectObj.el.remove(); | |
| if(document.getElementById('quad-svg')) document.getElementById('quad-svg').remove(); | |
| if(rotateHandle) { rotateHandle.remove(); rotateHandle = null; } | |
| if(quadRotateHandle) { quadRotateHandle.remove(); quadRotateHandle = null; } | |
| rectObj = null; quadPoints = null; setSelectedRect(false); | |
| } | |
| // --- SVG(自由四角形) --- | |
| function showQuadSVG() { | |
| let svg = document.getElementById('quad-svg'); | |
| if(svg) svg.remove(); | |
| svg = document.createElementNS("http://www.w3.org/2000/svg","svg"); | |
| svg.setAttribute("id","quad-svg"); | |
| svg.classList.add("quad-svg"); | |
| svg.setAttribute("width",container.clientWidth); | |
| svg.setAttribute("height",container.clientHeight); | |
| let poly = document.createElementNS("http://www.w3.org/2000/svg","polygon"); | |
| svg.appendChild(poly); | |
| container.appendChild(svg); | |
| updateQuadSVG(); | |
| } | |
| function updateQuadSVG() { | |
| let svg = document.getElementById('quad-svg'); | |
| if(!svg) showQuadSVG(); | |
| let poly = svg.querySelector('polygon'); | |
| if(quadPoints && poly) { | |
| let pts = quadPoints.map(p=>p.join(',')).join(' '); | |
| poly.setAttribute('points',pts); | |
| } | |
| } | |
| // --- 座標計算 --- | |
| function getRectImageCoords(rectObj) { | |
| let pts; | |
| if(currentMode==="quad" && quadPoints) { | |
| pts = quadPoints; | |
| } else { | |
| pts = getRectCornerPoints(rectObj); | |
| } | |
| const {left:imgL, top:imgT, width:imgW, height:imgH} = dispImgInfo; | |
| return pts.map(([rx, ry]) => [ | |
| Math.round((rx - imgL) * imgNaturalWidth / imgW), | |
| Math.round((ry - imgT) * imgNaturalHeight / imgH) | |
| ]); | |
| } | |
| function updateCoords(rectObj) { | |
| if (!rectObj) { coords.innerHTML = "Coordinates:<br>"; return; } | |
| const imgPoints = getRectImageCoords(rectObj); | |
| coords.innerHTML = | |
| `Original image pixel coordinates (top-left origin):<br> | |
| 1. (${imgPoints[0][0]}, ${imgPoints[0][1]})<br> | |
| 2. (${imgPoints[1][0]}, ${imgPoints[1][1]})<br> | |
| 3. (${imgPoints[2][0]}, ${imgPoints[2][1]})<br> | |
| 4. (${imgPoints[3][0]}, ${imgPoints[3][1]})<br>`; | |
| } | |
| function updateCoordsQuad() { | |
| updateCoords(rectObj); | |
| } | |
| // --- Memory --- | |
| saveRectBtn.addEventListener('click', () => { | |
| if (!rectObj) return; | |
| const imgPoints = getRectImageCoords(rectObj); | |
| memory.push({ | |
| filename: currentImageName || '', | |
| coords: imgPoints, | |
| id: Date.now() + Math.random() | |
| }); | |
| updateMemoryList(); | |
| clearRect(); | |
| coords.innerHTML = "Coordinates:<br>"; | |
| saveRectBtn.disabled = true; | |
| }); | |
| function updateMemoryList() { | |
| let html = ''; | |
| memory.forEach((mem) => { | |
| html += `<div class="memory-item" data-id="${mem.id}"> | |
| <span class="memory-label"><b>${mem.filename}</b> : | |
| TL(${mem.coords[0][0]},${mem.coords[0][1]}) | |
| TR(${mem.coords[1][0]},${mem.coords[1][1]}) | |
| BR(${mem.coords[2][0]},${mem.coords[2][1]}) | |
| BL(${mem.coords[3][0]},${mem.coords[3][1]}) | |
| </span> | |
| <button class="memory-delete-btn" data-id="${mem.id}" title="Delete this memory">×</button> | |
| </div>`; | |
| }); | |
| memoryList.innerHTML = html || "<span style='color:#999;'>No memory saved.</span>"; | |
| } | |
| memoryList.addEventListener('click', (e) => { | |
| if (e.target.classList.contains('memory-delete-btn')) { | |
| const id = e.target.dataset.id; | |
| memory = memory.filter(m => m.id != id); | |
| updateMemoryList(); | |
| } | |
| }); | |
| memorySaveBtn.addEventListener('click', async () => { | |
| if (!memory.length) { alert("No memory to save."); return; } | |
| let outObj = {}; | |
| memory.forEach(mem => { outObj[mem.filename] = { print_area: mem.coords }; }); | |
| let jsonString = JSON.stringify(outObj, null, 2); | |
| let filename = filenameInput.value.trim() || "templates.json"; | |
| filename = filename.replace(/[\\\/:*?"<>|]/g, "_"); | |
| if (window.showSaveFilePicker) { | |
| try { | |
| const opts = { | |
| suggestedName: filename, | |
| types: [{ description: 'JSON Files', accept: {'application/json': ['.json', '.txt']} }] | |
| }; | |
| const handle = await window.showSaveFilePicker(opts); | |
| const writable = await handle.createWritable(); | |
| await writable.write(jsonString); await writable.close(); | |
| alert("Memory saved!"); return; | |
| } catch (err) { if (err.name !== "AbortError") alert("Failed to save file: " + err.message); } | |
| } | |
| const blob = new Blob([jsonString], {type: "application/json"}); | |
| const url = URL.createObjectURL(blob); const a = document.createElement('a'); | |
| a.href = url; a.download = filename; document.body.appendChild(a); a.click(); | |
| setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 100); | |
| }); | |
| document.addEventListener('keydown', e => { | |
| if ((e.key === "Delete" || e.key === "Backspace") && rectObj) { | |
| clearRect(); | |
| setSelectedRect(false); | |
| coords.innerHTML = "Coordinates:<br>"; | |
| saveRectBtn.disabled = true; | |
| } | |
| }); | |
| coords.innerHTML = "Coordinates:<br>"; | |
| updateMemoryList(); | |
| updateFileNameLabel(""); | |
| </script> | |
| </body> | |
| </html> |