mockjet_web / coordinatemaker.html
junjiro1129's picture
Upload coordinatemaker.html
df1f96d verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Coordinate Maker</title>
<style>
#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;
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: 12px; height: 12px;
background: #2a7;
border-radius: 50%;
cursor: pointer;
margin: -6px 0 0 -6px;
z-index: 20;
border: 1px solid #fff;
box-shadow: 0 0 2px #0004;
display: none;
}
.rect.selected .handle {
display: block;
}
.rotate-handle {
position: absolute;
left: 50%;
top: -32px;
width: 18px;
height: 18px;
margin-left: -9px;
background: #ff0;
border: 2px solid #f90;
border-radius: 50%;
cursor: pointer;
z-index: 30;
display: none;
box-shadow: 0 0 4px #0006;
}
.rect.selected .rotate-handle {
display: block;
}
#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, #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;
}
#rect-list {
display: none;
}
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 !important;
}
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;">
</label>
<span id="file-name-label"></span>
<button id="add-rect-btn">Add Rectangle</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>
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 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');
let imgLoaded = false;
let imgNaturalWidth = 0, imgNaturalHeight = 0;
let dispImgInfo = {left:0, top:0, width:0, height:0};
let currentImageName = '';
let rectObj = null;
let drawing = false;
let moving = false;
let resizing = false;
let rotating = false;
let startX = 0, startY = 0;
let offsetX = 0, offsetY = 0;
let originX = 0, originY = 0;
let activeHandle = null;
let memory = [];
let rotateStartAngle = 0;
let rotateCenter = {x:0, y:0};
function updateFileNameLabel(name) {
fileNameLabel.textContent = name ? name : "";
}
function loadImageFromFile(file) {
if (!file || !file.type.match(/^image\//)) return;
const reader = new FileReader();
reader.onload = function(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(""); // Clear previous file name immediately
if (file) {
loadImageFromFile(file);
}
});
container.addEventListener('dragover', (e) => {
e.preventDefault();
e.stopPropagation();
container.classList.add('dragover');
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(""); // Clear previous name immediately
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;
const cH = container.clientHeight;
const iW = imgNaturalWidth;
const iH = imgNaturalHeight;
let dispW = cW, dispH = cH, dispL = 0, dispT = 0;
const imgAspect = iW / iH;
const 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);
addRectBtn.addEventListener('click', () => {
if (!imgLoaded) return;
if (rectObj) return;
drawing = true;
setSelectedRect(true);
coords.innerHTML = "Draw a rectangle on the image.<br>";
saveRectBtn.disabled = true;
});
saveRectBtn.addEventListener('click', () => {
if (!rectObj) return;
const imgPoints = getRectImageCoords(rectObj);
memory.push({
filename: currentImageName || '',
coords: imgPoints,
angle: rectObj.angle || 0,
id: Date.now() + Math.random()
});
updateMemoryList();
clearRect();
coords.innerHTML = "Coordinates:<br>";
saveRectBtn.disabled = true;
});
function clearRect() {
if (rectObj && rectObj.element) rectObj.element.remove();
rectObj = null;
setSelectedRect(false);
}
// ----- Main Rectangle Creation -----
container.addEventListener('mousedown', (e) => {
if (!imgLoaded) return;
if (!drawing) return;
if (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 rectEl = document.createElement('div');
rectEl.className = 'rect selected';
container.appendChild(rectEl);
rectObj = {
left: startX, top: startY, width: 0, height: 0,
element: rectEl, handles: [],
angle: 0
};
setSelectedRect(true);
updateRectUI(rectObj);
document.body.style.cursor = "crosshair";
drawing = true;
function onMouseMove(ev) {
const currX = ev.clientX - rectC.left;
const currY = ev.clientY - rectC.top;
rectObj.left = Math.min(startX, currX);
rectObj.top = Math.min(startY, currY);
rectObj.width = Math.abs(currX - startX);
rectObj.height = Math.abs(currY - startY);
updateRectUI(rectObj);
updateCoords(rectObj);
}
function onMouseUp(ev) {
drawing = false;
document.body.style.cursor = "";
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
if (rectObj.width < 10 || rectObj.height < 10) {
clearRect();
saveRectBtn.disabled = true;
} else {
createHandles(rectObj);
createRotateHandle(rectObj);
updateCoords(rectObj);
saveRectBtn.disabled = false;
}
}
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
e.preventDefault();
});
container.addEventListener('mousedown', (e) => {
if (drawing) return;
if (!imgLoaded) return;
if (!rectObj || !rectObj.element) return;
if (e.target === rectObj.element) {
setSelectedRect(true);
saveRectBtn.disabled = false;
moving = true;
const rectC = container.getBoundingClientRect();
offsetX = e.clientX - rectC.left - rectObj.left;
offsetY = e.clientY - rectC.top - rectObj.top;
document.body.style.cursor = "move";
function onMove(ev) {
let newLeft = ev.clientX - rectC.left - offsetX;
let newTop = ev.clientY - rectC.top - offsetY;
newLeft = Math.max(0, Math.min(newLeft, container.clientWidth - rectObj.width));
newTop = Math.max(0, Math.min(newTop, container.clientHeight - rectObj.height));
rectObj.left = newLeft;
rectObj.top = newTop;
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) ----
function createHandles(rectObj) {
if (rectObj.handles && rectObj.handles.length) {
rectObj.handles.forEach(h=>h.remove());
}
const positions = ['tl','tr','br','bl'];
rectObj.handles = [];
for(let i=0; i<4; i++) {
let h = document.createElement('div');
h.className = 'handle';
h.dataset.handle = positions[i];
h.style.cursor = handleCursor(positions[i]);
h.addEventListener('mousedown', (e) => {
resizing = true;
activeHandle = positions[i];
originX = e.clientX;
originY = e.clientY;
h._orig = {...rectObj};
h._orig.angle = rectObj.angle;
document.body.style.cursor = h.style.cursor;
function onMove(ev) {
resizeRect(rectObj, ev, h._orig);
updateRectUI(rectObj);
updateHandles(rectObj);
updateRotateHandle(rectObj);
updateCoords(rectObj);
}
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);
e.stopPropagation();
});
rectObj.element.appendChild(h);
rectObj.handles.push(h);
}
updateHandles(rectObj);
}
function handleCursor(pos) {
switch(pos) {
case 'tl': return 'nwse-resize';
case 'tr': return 'nesw-resize';
case 'br': return 'nwse-resize';
case 'bl': return 'nesw-resize';
default: return 'pointer';
}
}
function updateHandles(rectObj) {
const {width, height, handles} = rectObj;
if(!handles || handles.length!==4) return;
handles[0].style.left = '0px'; handles[0].style.top = '0px';
handles[1].style.left = width + 'px'; handles[1].style.top = '0px';
handles[2].style.left = width + 'px'; handles[2].style.top = height + 'px';
handles[3].style.left = '0px'; handles[3].style.top = height + 'px';
}
function resizeRect(rectObj, e, orig) {
// ignore angle for resizing, keep axis-aligned bounding box
let dx = e.clientX - originX;
let dy = e.clientY - originY;
let nd = {...orig};
switch(activeHandle) {
case 'tl':
nd.left = Math.min(orig.left + dx, orig.left + orig.width - 10);
nd.top = Math.min(orig.top + dy, orig.top + orig.height - 10);
nd.width = orig.width - (nd.left - orig.left);
nd.height = orig.height - (nd.top - orig.top);
break;
case 'tr':
nd.top = Math.min(orig.top + dy, orig.top + orig.height - 10);
nd.width = Math.max(10, orig.width + dx);
nd.height = orig.height - (nd.top - orig.top);
nd.left = orig.left;
break;
case 'br':
nd.width = Math.max(10, orig.width + dx);
nd.height = Math.max(10, orig.height + dy);
nd.left = orig.left;
nd.top = orig.top;
break;
case 'bl':
nd.left = Math.min(orig.left + dx, orig.left + orig.width - 10);
nd.width = orig.width - (nd.left - orig.left);
nd.height = Math.max(10, orig.height + dy);
nd.top = orig.top;
break;
}
nd.left = Math.max(0, Math.min(nd.left, container.clientWidth - nd.width));
nd.top = Math.max(0, Math.min(nd.top, container.clientHeight - nd.height));
nd.width = Math.max(10, Math.min(nd.width, container.clientWidth - nd.left));
nd.height = Math.max(10, Math.min(nd.height, container.clientHeight - nd.top));
Object.assign(rectObj, nd);
}
// ---- Rotation Handle ----
function createRotateHandle(rectObj) {
if(rectObj.rotateHandle) rectObj.rotateHandle.remove();
let h = document.createElement('div');
h.className = 'rotate-handle';
rectObj.element.appendChild(h);
rectObj.rotateHandle = h;
updateRotateHandle(rectObj);
h.addEventListener('mousedown', function(e) {
rotating = true;
const rectC = container.getBoundingClientRect();
// center of rectangle in container coords
let cx = rectObj.left + rectObj.width/2;
let cy = rectObj.top + rectObj.height/2;
rotateCenter = {x: cx, y: cy};
// 角度初期値
const mx = e.clientX - rectC.left, my = e.clientY - rectC.top;
rotateStartAngle = Math.atan2(my - cy, mx - cx) * 180/Math.PI - (rectObj.angle||0);
document.body.style.cursor = "crosshair";
function onMove(ev) {
const mx2 = ev.clientX - rectC.left, my2 = ev.clientY - rectC.top;
let angle = Math.atan2(my2 - cy, mx2 - cx) * 180/Math.PI - rotateStartAngle;
// angleを0-360に正規化
angle = ((angle % 360) + 360) % 360;
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) {
if(!rectObj.rotateHandle) return;
// 回転ハンドルの位置は矩形の中心上方向に固定
let w = rectObj.width, h = rectObj.height;
let angle = rectObj.angle || 0;
// 中心
let cx = w/2, cy = h/2;
// ハンドルの相対座標(矩形の中心から上へ)
let r = Math.max(w, h)/2 + 24;
let rad = (-90 + angle) * Math.PI / 180.0;
let hx = cx + r * Math.cos(rad);
let hy = cy + r * Math.sin(rad);
rectObj.rotateHandle.style.left = `${hx}px`;
rectObj.rotateHandle.style.top = `${hy}px`;
}
// --- 矩形のUI更新: 回転も反映
function updateRectUI(rectObj) {
rectObj.element.style.left = rectObj.left + 'px';
rectObj.element.style.top = rectObj.top + 'px';
rectObj.element.style.width = rectObj.width + 'px';
rectObj.element.style.height = rectObj.height + 'px';
rectObj.element.classList.add('selected');
rectObj.angle = typeof rectObj.angle === "number" ? rectObj.angle : 0;
rectObj.element.style.transform = `rotate(${rectObj.angle||0}deg)`;
rectObj.element.style.transformOrigin = "50% 50%";
}
function setSelectedRect(selected) {
if (rectObj && rectObj.element) {
if (selected) {
rectObj.element.classList.add('selected');
} else {
rectObj.element.classList.remove('selected');
}
}
saveRectBtn.disabled = !rectObj;
}
function getRectImageCoords(rectObj) {
const {left:imgL, top:imgT, width:imgW, height:imgH} = dispImgInfo;
let x = rectObj.left, y = rectObj.top, w = rectObj.width, h = rectObj.height;
// 中心
let cx = x + w/2, cy = y + h/2;
// 角度
let angle = ((typeof rectObj.angle === "number" ? rectObj.angle : 0) * Math.PI) / 180;
// 各頂点
let box = [
[x, y ], // TL
[x+w, y ], // TR
[x+w, y+h ], // BR
[x, y+h ] // BL
];
// 回転適用
let rot = box.map(([px,py]) => {
let dx = px - cx, dy = py - cy;
let rx = dx * Math.cos(angle) - dy * Math.sin(angle) + cx;
let ry = dx * Math.sin(angle) + dy * Math.cos(angle) + cy;
// img領域→元画像座標
let ix = Math.round((rx - imgL) * imgNaturalWidth / imgW);
let iy = Math.round((ry - imgT) * imgNaturalHeight / imgH);
return [ix, iy];
});
return rot;
}
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>
[angle: ${rectObj.angle ? rectObj.angle.toFixed(1) : 0}°]`;
}
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]})
[angle: ${mem.angle || 0}°]
</span>
<button class="memory-delete-btn" data-id="${mem.id}" title="Delete this memory">&times;</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();
}
});
// Save memory to JSON file using the File System Access API if available (showSaveFilePicker)
memorySaveBtn.addEventListener('click', async () => {
if (!memory.length) {
alert("No memory to save.");
return;
}
// ★ 画像名に「bases/」を付けないように修正
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>