選択モードの追加(独自実装)
This commit is contained in:
parent
1a2e5ff342
commit
78aaf9edeb
@ -1,4 +1,7 @@
|
|||||||
# imagemarkpengent README
|
# imagemarkpengent
|
||||||
|
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
This is the README for your extension "imagemarkpengent". After writing up a brief description, we recommend including the following sections.
|
This is the README for your extension "imagemarkpengent". After writing up a brief description, we recommend including the following sections.
|
||||||
|
|
||||||
|
BIN
readme/images/top.png
Normal file
BIN
readme/images/top.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 65 KiB |
@ -28,6 +28,7 @@ export function getWebviewContent(imageSrc: string): string {
|
|||||||
<body>
|
<body>
|
||||||
<div id="toolbar">
|
<div id="toolbar">
|
||||||
<button id="moveBtn" class="active-btn">移動モード</button>
|
<button id="moveBtn" class="active-btn">移動モード</button>
|
||||||
|
<button id="selectBtn">選択モード</button>
|
||||||
<button id="markBtn">マーク追加(Ctr+左クリック)</button>
|
<button id="markBtn">マーク追加(Ctr+左クリック)</button>
|
||||||
<label>太さ:
|
<label>太さ:
|
||||||
<select id="lineWidth">
|
<select id="lineWidth">
|
||||||
@ -49,12 +50,13 @@ export function getWebviewContent(imageSrc: string): string {
|
|||||||
|
|
||||||
// UI要素
|
// UI要素
|
||||||
const moveBtn = document.getElementById('moveBtn');
|
const moveBtn = document.getElementById('moveBtn');
|
||||||
|
const selectBtn = document.getElementById('selectBtn');
|
||||||
const markBtn = document.getElementById('markBtn');
|
const markBtn = document.getElementById('markBtn');
|
||||||
const lineWidthSelect = document.getElementById('lineWidth');
|
const lineWidthSelect = document.getElementById('lineWidth');
|
||||||
const colorPicker = document.getElementById('colorPicker');
|
const colorPicker = document.getElementById('colorPicker');
|
||||||
|
|
||||||
// 状態
|
// 状態
|
||||||
let mode = 'move'; // 'move' or 'mark'
|
let mode = 'move'; // 'move' or 'mark' or 'select'
|
||||||
let scale = 1;
|
let scale = 1;
|
||||||
let minScale = 0.1;
|
let minScale = 0.1;
|
||||||
let maxScale = 10;
|
let maxScale = 10;
|
||||||
@ -66,6 +68,14 @@ export function getWebviewContent(imageSrc: string): string {
|
|||||||
let lastOffsetX = 0;
|
let lastOffsetX = 0;
|
||||||
let lastOffsetY = 0;
|
let lastOffsetY = 0;
|
||||||
let ctrlPressed = false;
|
let ctrlPressed = false;
|
||||||
|
// 図形移動用
|
||||||
|
let isMarkMoving = false;
|
||||||
|
let markMoveStart = null;
|
||||||
|
let markMoveOrigin = null;
|
||||||
|
// 図形リサイズ用
|
||||||
|
let isMarkResizing = false;
|
||||||
|
let resizeTarget = null; // {mark, handleIndex}
|
||||||
|
let resizeStart = null; // {x, y, orig}
|
||||||
|
|
||||||
// マーク(楕円)の配列
|
// マーク(楕円)の配列
|
||||||
let marks = [];
|
let marks = [];
|
||||||
@ -79,24 +89,54 @@ export function getWebviewContent(imageSrc: string): string {
|
|||||||
|
|
||||||
// UIイベント
|
// UIイベント
|
||||||
moveBtn.onclick = () => setMode('move');
|
moveBtn.onclick = () => setMode('move');
|
||||||
|
selectBtn.onclick = () => setMode('select');
|
||||||
markBtn.onclick = () => setMode('mark');
|
markBtn.onclick = () => setMode('mark');
|
||||||
lineWidthSelect.onchange = () => { markLineWidth = parseInt(lineWidthSelect.value, 10); draw(); };
|
lineWidthSelect.onchange = () => {
|
||||||
colorPicker.oninput = () => { markColor = colorPicker.value; draw(); };
|
markLineWidth = parseInt(lineWidthSelect.value, 10);
|
||||||
|
let changed = false;
|
||||||
|
for (const mark of marks) {
|
||||||
|
if (mark.isSelected) {
|
||||||
|
mark.lineWidth = markLineWidth;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changed) pushUndo();
|
||||||
|
draw();
|
||||||
|
};
|
||||||
|
colorPicker.oninput = () => {
|
||||||
|
markColor = colorPicker.value;
|
||||||
|
let changed = false;
|
||||||
|
for (const mark of marks) {
|
||||||
|
if (mark.isSelected) {
|
||||||
|
mark.color = markColor;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changed) pushUndo();
|
||||||
|
draw();
|
||||||
|
};
|
||||||
|
|
||||||
function setMode(newMode) {
|
function setMode(newMode) {
|
||||||
mode = newMode;
|
mode = newMode;
|
||||||
|
moveBtn.classList.remove('active-btn');
|
||||||
|
selectBtn.classList.remove('active-btn');
|
||||||
|
markBtn.classList.remove('active-btn');
|
||||||
if (mode === 'move') {
|
if (mode === 'move') {
|
||||||
moveBtn.classList.add('active-btn');
|
moveBtn.classList.add('active-btn');
|
||||||
markBtn.classList.remove('active-btn');
|
|
||||||
canvas.style.cursor = 'grab';
|
canvas.style.cursor = 'grab';
|
||||||
|
} else if (mode === 'select') {
|
||||||
|
selectBtn.classList.add('active-btn');
|
||||||
|
canvas.style.cursor = 'pointer';
|
||||||
} else {
|
} else {
|
||||||
moveBtn.classList.remove('active-btn');
|
|
||||||
markBtn.classList.add('active-btn');
|
markBtn.classList.add('active-btn');
|
||||||
canvas.style.cursor = 'crosshair';
|
canvas.style.cursor = 'crosshair';
|
||||||
}
|
}
|
||||||
// プレビュー消去
|
|
||||||
tempMark = null;
|
tempMark = null;
|
||||||
markStart = null;
|
markStart = null;
|
||||||
|
// 選択解除
|
||||||
|
if (mode !== 'select') {
|
||||||
|
for (const mark of marks) mark.isSelected = false;
|
||||||
|
}
|
||||||
draw();
|
draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,6 +152,19 @@ export function getWebviewContent(imageSrc: string): string {
|
|||||||
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'z') {
|
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'z') {
|
||||||
undo();
|
undo();
|
||||||
}
|
}
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'y') {
|
||||||
|
redo();
|
||||||
|
}
|
||||||
|
// 選択モードでDeleteキー
|
||||||
|
if (mode === 'select' && (e.key === 'Delete' || e.key === 'Backspace')) {
|
||||||
|
const before = marks.length;
|
||||||
|
const newMarks = marks.filter(m => !m.isSelected);
|
||||||
|
if (newMarks.length !== marks.length) {
|
||||||
|
pushUndo();
|
||||||
|
marks = newMarks;
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
window.addEventListener('keyup', (e) => {
|
window.addEventListener('keyup', (e) => {
|
||||||
if (e.key === 'Control') {
|
if (e.key === 'Control') {
|
||||||
@ -147,6 +200,10 @@ export function getWebviewContent(imageSrc: string): string {
|
|||||||
// マークをすべて描画
|
// マークをすべて描画
|
||||||
for (const mark of marks) {
|
for (const mark of marks) {
|
||||||
drawEllipse(mark.x1, mark.y1, mark.x2, mark.y2, mark.color, mark.lineWidth);
|
drawEllipse(mark.x1, mark.y1, mark.x2, mark.y2, mark.color, mark.lineWidth);
|
||||||
|
if (mark.isSelected) {
|
||||||
|
drawEllipse(mark.x1, mark.y1, mark.x2, mark.y2, '#1976d2', (mark.lineWidth + 4), true);
|
||||||
|
drawResizeHandles(mark);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// プレビュー中のマーク
|
// プレビュー中のマーク
|
||||||
if (tempMark) {
|
if (tempMark) {
|
||||||
@ -171,6 +228,42 @@ export function getWebviewContent(imageSrc: string): string {
|
|||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// リサイズハンドル描画
|
||||||
|
function drawResizeHandles(mark) {
|
||||||
|
const handles = getHandlePositions(mark);
|
||||||
|
for (const h of handles) {
|
||||||
|
ctx.save();
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.rect(h.x - 5, h.y - 5, 10, 10);
|
||||||
|
ctx.fillStyle = '#fff';
|
||||||
|
ctx.strokeStyle = '#1976d2';
|
||||||
|
ctx.lineWidth = 2 / scale;
|
||||||
|
ctx.fill();
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 四隅のハンドル座標取得
|
||||||
|
function getHandlePositions(mark) {
|
||||||
|
return [
|
||||||
|
{ x: mark.x1, y: mark.y1 }, // 左上
|
||||||
|
{ x: mark.x2, y: mark.y1 }, // 右上
|
||||||
|
{ x: mark.x2, y: mark.y2 }, // 右下
|
||||||
|
{ x: mark.x1, y: mark.y2 }, // 左下
|
||||||
|
];
|
||||||
|
}
|
||||||
|
// ハンドル上か判定
|
||||||
|
function getHandleAt(x, y, mark) {
|
||||||
|
const handles = getHandlePositions(mark);
|
||||||
|
for (let i = 0; i < handles.length; i++) {
|
||||||
|
const h = handles[i];
|
||||||
|
if (Math.abs(x - h.x) < 10 / scale && Math.abs(y - h.y) < 10 / scale) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
// ホイールでズーム
|
// ホイールでズーム
|
||||||
canvas.addEventListener('wheel', (e) => {
|
canvas.addEventListener('wheel', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -188,7 +281,7 @@ export function getWebviewContent(imageSrc: string): string {
|
|||||||
draw();
|
draw();
|
||||||
}, { passive: false });
|
}, { passive: false });
|
||||||
|
|
||||||
// --- マーク追加モード or Ctrl+ドラッグで楕円追加 ---
|
// --- マーク追加モード or Ctrl+ドラッグで楕円追加 or 選択モードで選択 ---
|
||||||
canvas.addEventListener('mousedown', (e) => {
|
canvas.addEventListener('mousedown', (e) => {
|
||||||
const rect = canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
const x = (e.clientX - rect.left - offsetX) / scale;
|
const x = (e.clientX - rect.left - offsetX) / scale;
|
||||||
@ -198,13 +291,66 @@ export function getWebviewContent(imageSrc: string): string {
|
|||||||
markStart = { x1: x, y1: y };
|
markStart = { x1: x, y1: y };
|
||||||
tempMark = { x1: x, y1: y, x2: x, y2: y, color: markColor, lineWidth: markLineWidth };
|
tempMark = { x1: x, y1: y, x2: x, y2: y, color: markColor, lineWidth: markLineWidth };
|
||||||
}
|
}
|
||||||
|
} else if (mode === 'select') {
|
||||||
|
// 先にリサイズハンドル判定
|
||||||
|
let resized = false;
|
||||||
|
for (const mark of marks) {
|
||||||
|
if (mark.isSelected) {
|
||||||
|
const handleIdx = getHandleAt(x, y, mark);
|
||||||
|
if (handleIdx !== -1) {
|
||||||
|
isMarkResizing = true;
|
||||||
|
resizeTarget = { mark, handleIdx };
|
||||||
|
resizeStart = { x, y, orig: { x1: mark.x1, y1: mark.y1, x2: mark.x2, y2: mark.y2 } };
|
||||||
|
resized = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (resized) return;
|
||||||
|
// 既存マークの選択判定
|
||||||
|
let found = false;
|
||||||
|
for (let i = marks.length - 1; i >= 0; i--) { // 上に描画されているもの優先
|
||||||
|
const mark = marks[i];
|
||||||
|
if (isPointInEllipse(x, y, mark)) {
|
||||||
|
for (const m of marks) m.isSelected = false;
|
||||||
|
mark.isSelected = true;
|
||||||
|
found = true;
|
||||||
|
// 選択中マークの移動開始
|
||||||
|
isMarkMoving = true;
|
||||||
|
markMoveStart = { x, y };
|
||||||
|
// 複数選択対応のため配列で保存
|
||||||
|
markMoveOrigin = marks.filter(m => m.isSelected).map(m => ({ x1: m.x1, y1: m.y1, x2: m.x2, y2: m.y2 }));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
for (const m of marks) m.isSelected = false;
|
||||||
|
isMarkMoving = false;
|
||||||
|
markMoveStart = null;
|
||||||
|
markMoveOrigin = null;
|
||||||
|
}
|
||||||
|
draw();
|
||||||
} else if (mode === 'move') {
|
} else if (mode === 'move') {
|
||||||
isDragging = true;
|
// 追加: マーク上なら選択モードに切り替え
|
||||||
dragStartX = e.clientX;
|
let found = false;
|
||||||
dragStartY = e.clientY;
|
for (let i = marks.length - 1; i >= 0; i--) {
|
||||||
lastOffsetX = offsetX;
|
const mark = marks[i];
|
||||||
lastOffsetY = offsetY;
|
if (isPointInEllipse(x, y, mark)) {
|
||||||
canvas.style.cursor = 'grabbing';
|
for (const m of marks) m.isSelected = false;
|
||||||
|
mark.isSelected = true;
|
||||||
|
setMode('select');
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
isDragging = true;
|
||||||
|
dragStartX = e.clientX;
|
||||||
|
dragStartY = e.clientY;
|
||||||
|
lastOffsetX = offsetX;
|
||||||
|
lastOffsetY = offsetY;
|
||||||
|
canvas.style.cursor = 'grabbing';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
window.addEventListener('mousemove', (e) => {
|
window.addEventListener('mousemove', (e) => {
|
||||||
@ -214,6 +360,42 @@ export function getWebviewContent(imageSrc: string): string {
|
|||||||
if ((mode === 'mark' || ctrlPressed) && markStart) {
|
if ((mode === 'mark' || ctrlPressed) && markStart) {
|
||||||
tempMark = { x1: markStart.x1, y1: markStart.y1, x2: x, y2: y, color: markColor, lineWidth: markLineWidth };
|
tempMark = { x1: markStart.x1, y1: markStart.y1, x2: x, y2: y, color: markColor, lineWidth: markLineWidth };
|
||||||
draw();
|
draw();
|
||||||
|
} else if (mode === 'select' && isMarkResizing && resizeTarget && resizeStart) {
|
||||||
|
// リサイズ処理
|
||||||
|
const { mark, handleIdx } = resizeTarget;
|
||||||
|
const dx = x - resizeStart.x;
|
||||||
|
const dy = y - resizeStart.y;
|
||||||
|
// ハンドルごとに座標を調整
|
||||||
|
let { x1, y1, x2, y2 } = resizeStart.orig;
|
||||||
|
if (handleIdx === 0) { // 左上
|
||||||
|
mark.x1 = x1 + dx;
|
||||||
|
mark.y1 = y1 + dy;
|
||||||
|
} else if (handleIdx === 1) { // 右上
|
||||||
|
mark.x2 = x2 + dx;
|
||||||
|
mark.y1 = y1 + dy;
|
||||||
|
} else if (handleIdx === 2) { // 右下
|
||||||
|
mark.x2 = x2 + dx;
|
||||||
|
mark.y2 = y2 + dy;
|
||||||
|
} else if (handleIdx === 3) { // 左下
|
||||||
|
mark.x1 = x1 + dx;
|
||||||
|
mark.y2 = y2 + dy;
|
||||||
|
}
|
||||||
|
draw();
|
||||||
|
} else if (mode === 'select' && isMarkMoving && markMoveStart && markMoveOrigin) {
|
||||||
|
// 選択中図形の移動
|
||||||
|
const dx = x - markMoveStart.x;
|
||||||
|
const dy = y - markMoveStart.y;
|
||||||
|
let idx = 0;
|
||||||
|
for (let i = 0; i < marks.length; i++) {
|
||||||
|
if (marks[i].isSelected) {
|
||||||
|
const orig = markMoveOrigin[idx++];
|
||||||
|
marks[i].x1 = orig.x1 + dx;
|
||||||
|
marks[i].y1 = orig.y1 + dy;
|
||||||
|
marks[i].x2 = orig.x2 + dx;
|
||||||
|
marks[i].y2 = orig.y2 + dy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
draw();
|
||||||
} else if (mode === 'move' && isDragging) {
|
} else if (mode === 'move' && isDragging) {
|
||||||
offsetX = lastOffsetX + (e.clientX - dragStartX);
|
offsetX = lastOffsetX + (e.clientX - dragStartX);
|
||||||
offsetY = lastOffsetY + (e.clientY - dragStartY);
|
offsetY = lastOffsetY + (e.clientY - dragStartY);
|
||||||
@ -224,12 +406,24 @@ export function getWebviewContent(imageSrc: string): string {
|
|||||||
if ((mode === 'mark' || ctrlPressed) && markStart && tempMark) {
|
if ((mode === 'mark' || ctrlPressed) && markStart && tempMark) {
|
||||||
if (Math.abs(tempMark.x2 - tempMark.x1) >= 2 / scale && Math.abs(tempMark.y2 - tempMark.y1) >= 2 / scale) {
|
if (Math.abs(tempMark.x2 - tempMark.x1) >= 2 / scale && Math.abs(tempMark.y2 - tempMark.y1) >= 2 / scale) {
|
||||||
pushUndo();
|
pushUndo();
|
||||||
marks.push({ ...tempMark });
|
marks.push({ ...tempMark, isSelected: false });
|
||||||
}
|
}
|
||||||
markStart = null;
|
markStart = null;
|
||||||
tempMark = null;
|
tempMark = null;
|
||||||
if (mode === 'mark') setMode('move');
|
if (mode === 'mark') setMode('move');
|
||||||
draw();
|
draw();
|
||||||
|
} else if (mode === 'select' && isMarkResizing) {
|
||||||
|
isMarkResizing = false;
|
||||||
|
resizeTarget = null;
|
||||||
|
resizeStart = null;
|
||||||
|
pushUndo();
|
||||||
|
draw();
|
||||||
|
} else if (mode === 'select' && isMarkMoving) {
|
||||||
|
isMarkMoving = false;
|
||||||
|
markMoveStart = null;
|
||||||
|
markMoveOrigin = null;
|
||||||
|
pushUndo();
|
||||||
|
draw();
|
||||||
} else if (mode === 'move' && isDragging) {
|
} else if (mode === 'move' && isDragging) {
|
||||||
isDragging = false;
|
isDragging = false;
|
||||||
canvas.style.cursor = 'grab';
|
canvas.style.cursor = 'grab';
|
||||||
@ -237,19 +431,41 @@ export function getWebviewContent(imageSrc: string): string {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let undoStack = [];
|
let undoStack = [];
|
||||||
|
let redoStack = [];
|
||||||
function pushUndo() {
|
function pushUndo() {
|
||||||
undoStack.push(JSON.stringify(marks));
|
undoStack.push(JSON.stringify(marks));
|
||||||
if (undoStack.length > 100) undoStack.shift();
|
if (undoStack.length > 100) undoStack.shift();
|
||||||
|
redoStack = []; // Undoした後に新しい操作があればRedo履歴は消す
|
||||||
}
|
}
|
||||||
function undo() {
|
function undo() {
|
||||||
if (undoStack.length > 0) {
|
if (undoStack.length > 1) {
|
||||||
|
redoStack.push(JSON.stringify(marks));
|
||||||
marks = JSON.parse(undoStack.pop());
|
marks = JSON.parse(undoStack.pop());
|
||||||
draw();
|
draw();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function redo() {
|
||||||
|
if (redoStack.length > 0) {
|
||||||
|
undoStack.push(JSON.stringify(marks));
|
||||||
|
marks = JSON.parse(redoStack.pop());
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 楕円内判定関数
|
||||||
|
function isPointInEllipse(px, py, mark) {
|
||||||
|
const cx = (mark.x1 + mark.x2) / 2;
|
||||||
|
const cy = (mark.y1 + mark.y2) / 2;
|
||||||
|
const rx = Math.abs(mark.x2 - mark.x1) / 2;
|
||||||
|
const ry = Math.abs(mark.y2 - mark.y1) / 2;
|
||||||
|
if (rx < 1e-2 || ry < 1e-2) return false;
|
||||||
|
return ((px - cx) ** 2) / (rx ** 2) + ((py - cy) ** 2) / (ry ** 2) <= 1;
|
||||||
|
}
|
||||||
|
|
||||||
// 初期リサイズ
|
// 初期リサイズ
|
||||||
resizeCanvas();
|
resizeCanvas();
|
||||||
|
// 最初の状態をundoStackにpush
|
||||||
|
pushUndo();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user