2画面とUNDO機能、基点変更
This commit is contained in:
parent
38c4e25733
commit
1a2e5ff342
@ -28,7 +28,7 @@ export function activate(context: vscode.ExtensionContext) {
|
|||||||
const panel = vscode.window.createWebviewPanel(
|
const panel = vscode.window.createWebviewPanel(
|
||||||
'imageEditor',
|
'imageEditor',
|
||||||
'ImageMarkPengent',
|
'ImageMarkPengent',
|
||||||
vscode.ViewColumn.One,
|
vscode.ViewColumn.Two,
|
||||||
{
|
{
|
||||||
enableScripts: true,
|
enableScripts: true,
|
||||||
localResourceRoots: [vscode.Uri.file(path.dirname(uri.fsPath))]
|
localResourceRoots: [vscode.Uri.file(path.dirname(uri.fsPath))]
|
||||||
|
@ -7,10 +7,39 @@ export function getWebviewContent(imageSrc: string): string {
|
|||||||
<title>ImageMarkPengent</title>
|
<title>ImageMarkPengent</title>
|
||||||
<style>
|
<style>
|
||||||
html, body { height: 100%; margin: 0; padding: 0; overflow: hidden; }
|
html, body { height: 100%; margin: 0; padding: 0; overflow: hidden; }
|
||||||
|
#toolbar {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
left: 10px;
|
||||||
|
background: rgba(255,255,255,0.95);
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 2px 8px #0002;
|
||||||
|
padding: 8px 12px;
|
||||||
|
z-index: 10;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
#canvas { display: block; width: 100vw; height: 100vh; background: #222; cursor: grab; }
|
#canvas { display: block; width: 100vw; height: 100vh; background: #222; cursor: grab; }
|
||||||
|
.active-btn { background: #1976d2; color: #fff; border-radius: 4px; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div id="toolbar">
|
||||||
|
<button id="moveBtn" class="active-btn">移動モード</button>
|
||||||
|
<button id="markBtn">マーク追加(Ctr+左クリック)</button>
|
||||||
|
<label>太さ:
|
||||||
|
<select id="lineWidth">
|
||||||
|
<option value="2">2px</option>
|
||||||
|
<option value="4">4px</option>
|
||||||
|
<option value="8">8px</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>色:
|
||||||
|
<input type="color" id="colorPicker" value="#ff0000" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<canvas id="canvas"></canvas>
|
<canvas id="canvas"></canvas>
|
||||||
<script>
|
<script>
|
||||||
const canvas = document.getElementById('canvas');
|
const canvas = document.getElementById('canvas');
|
||||||
@ -18,7 +47,14 @@ export function getWebviewContent(imageSrc: string): string {
|
|||||||
let img = new Image();
|
let img = new Image();
|
||||||
img.src = "${imageSrc}";
|
img.src = "${imageSrc}";
|
||||||
|
|
||||||
// ズーム・パン用変数
|
// UI要素
|
||||||
|
const moveBtn = document.getElementById('moveBtn');
|
||||||
|
const markBtn = document.getElementById('markBtn');
|
||||||
|
const lineWidthSelect = document.getElementById('lineWidth');
|
||||||
|
const colorPicker = document.getElementById('colorPicker');
|
||||||
|
|
||||||
|
// 状態
|
||||||
|
let mode = 'move'; // 'move' or 'mark'
|
||||||
let scale = 1;
|
let scale = 1;
|
||||||
let minScale = 0.1;
|
let minScale = 0.1;
|
||||||
let maxScale = 10;
|
let maxScale = 10;
|
||||||
@ -29,6 +65,60 @@ export function getWebviewContent(imageSrc: string): string {
|
|||||||
let dragStartY = 0;
|
let dragStartY = 0;
|
||||||
let lastOffsetX = 0;
|
let lastOffsetX = 0;
|
||||||
let lastOffsetY = 0;
|
let lastOffsetY = 0;
|
||||||
|
let ctrlPressed = false;
|
||||||
|
|
||||||
|
// マーク(楕円)の配列
|
||||||
|
let marks = [];
|
||||||
|
// 一時的なマーク(ドラッグ中のプレビュー)
|
||||||
|
let tempMark = null;
|
||||||
|
let markStart = null;
|
||||||
|
|
||||||
|
// 設定
|
||||||
|
let markColor = colorPicker.value;
|
||||||
|
let markLineWidth = parseInt(lineWidthSelect.value, 10);
|
||||||
|
|
||||||
|
// UIイベント
|
||||||
|
moveBtn.onclick = () => setMode('move');
|
||||||
|
markBtn.onclick = () => setMode('mark');
|
||||||
|
lineWidthSelect.onchange = () => { markLineWidth = parseInt(lineWidthSelect.value, 10); draw(); };
|
||||||
|
colorPicker.oninput = () => { markColor = colorPicker.value; draw(); };
|
||||||
|
|
||||||
|
function setMode(newMode) {
|
||||||
|
mode = newMode;
|
||||||
|
if (mode === 'move') {
|
||||||
|
moveBtn.classList.add('active-btn');
|
||||||
|
markBtn.classList.remove('active-btn');
|
||||||
|
canvas.style.cursor = 'grab';
|
||||||
|
} else {
|
||||||
|
moveBtn.classList.remove('active-btn');
|
||||||
|
markBtn.classList.add('active-btn');
|
||||||
|
canvas.style.cursor = 'crosshair';
|
||||||
|
}
|
||||||
|
// プレビュー消去
|
||||||
|
tempMark = null;
|
||||||
|
markStart = null;
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrlキー押下・離上で一時的にマーク追加モード
|
||||||
|
window.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Control') {
|
||||||
|
ctrlPressed = true;
|
||||||
|
canvas.style.cursor = 'crosshair';
|
||||||
|
}
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
setMode('move');
|
||||||
|
}
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'z') {
|
||||||
|
undo();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
window.addEventListener('keyup', (e) => {
|
||||||
|
if (e.key === 'Control') {
|
||||||
|
ctrlPressed = false;
|
||||||
|
if (mode === 'move') canvas.style.cursor = 'grab';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// リサイズ対応
|
// リサイズ対応
|
||||||
function resizeCanvas() {
|
function resizeCanvas() {
|
||||||
@ -40,11 +130,9 @@ export function getWebviewContent(imageSrc: string): string {
|
|||||||
|
|
||||||
// 画像ロード後、初期ズーム・位置を計算
|
// 画像ロード後、初期ズーム・位置を計算
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
// 画像全体がcanvasに収まるように初期scaleを計算
|
|
||||||
const scaleX = canvas.width / img.width;
|
const scaleX = canvas.width / img.width;
|
||||||
const scaleY = canvas.height / img.height;
|
const scaleY = canvas.height / img.height;
|
||||||
scale = Math.min(scaleX, scaleY, 1);
|
scale = Math.min(scaleX, scaleY, 1);
|
||||||
// 画像が中央に来るように初期オフセット
|
|
||||||
offsetX = (canvas.width - img.width * scale) / 2;
|
offsetX = (canvas.width - img.width * scale) / 2;
|
||||||
offsetY = (canvas.height - img.height * scale) / 2;
|
offsetY = (canvas.height - img.height * scale) / 2;
|
||||||
draw();
|
draw();
|
||||||
@ -56,6 +144,30 @@ export function getWebviewContent(imageSrc: string): string {
|
|||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.setTransform(scale, 0, 0, scale, offsetX, offsetY);
|
ctx.setTransform(scale, 0, 0, scale, offsetX, offsetY);
|
||||||
ctx.drawImage(img, 0, 0);
|
ctx.drawImage(img, 0, 0);
|
||||||
|
// マークをすべて描画
|
||||||
|
for (const mark of marks) {
|
||||||
|
drawEllipse(mark.x1, mark.y1, mark.x2, mark.y2, mark.color, mark.lineWidth);
|
||||||
|
}
|
||||||
|
// プレビュー中のマーク
|
||||||
|
if (tempMark) {
|
||||||
|
drawEllipse(tempMark.x1, tempMark.y1, tempMark.x2, tempMark.y2, tempMark.color, tempMark.lineWidth, true);
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawEllipse(x1, y1, x2, y2, color, lineWidth, dashed = false) {
|
||||||
|
ctx.save();
|
||||||
|
ctx.beginPath();
|
||||||
|
const cx = (x1 + x2) / 2;
|
||||||
|
const cy = (y1 + y2) / 2;
|
||||||
|
const rx = Math.abs(x2 - x1) / 2;
|
||||||
|
const ry = Math.abs(y2 - y1) / 2;
|
||||||
|
ctx.ellipse(cx, cy, rx, ry, 0, 0, 2 * Math.PI);
|
||||||
|
ctx.strokeStyle = color;
|
||||||
|
ctx.lineWidth = lineWidth / scale;
|
||||||
|
if (dashed) ctx.setLineDash([6 / scale, 6 / scale]);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.setLineDash([]);
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,32 +183,71 @@ export function getWebviewContent(imageSrc: string): string {
|
|||||||
scale /= 1.1;
|
scale /= 1.1;
|
||||||
}
|
}
|
||||||
scale = Math.max(minScale, Math.min(maxScale, scale));
|
scale = Math.max(minScale, Math.min(maxScale, scale));
|
||||||
// ズーム位置をマウス座標中心に
|
|
||||||
offsetX = mouseX - ((mouseX - offsetX) * (scale / prevScale));
|
offsetX = mouseX - ((mouseX - offsetX) * (scale / prevScale));
|
||||||
offsetY = mouseY - ((mouseY - offsetY) * (scale / prevScale));
|
offsetY = mouseY - ((mouseY - offsetY) * (scale / prevScale));
|
||||||
draw();
|
draw();
|
||||||
}, { passive: false });
|
}, { passive: false });
|
||||||
|
|
||||||
// ドラッグでパン
|
// --- マーク追加モード or Ctrl+ドラッグで楕円追加 ---
|
||||||
canvas.addEventListener('mousedown', (e) => {
|
canvas.addEventListener('mousedown', (e) => {
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const x = (e.clientX - rect.left - offsetX) / scale;
|
||||||
|
const y = (e.clientY - rect.top - offsetY) / scale;
|
||||||
|
if (mode === 'mark' || ctrlPressed) {
|
||||||
|
if (x >= 0 && y >= 0 && x <= img.width && y <= img.height) {
|
||||||
|
markStart = { x1: x, y1: y };
|
||||||
|
tempMark = { x1: x, y1: y, x2: x, y2: y, color: markColor, lineWidth: markLineWidth };
|
||||||
|
}
|
||||||
|
} else if (mode === 'move') {
|
||||||
isDragging = true;
|
isDragging = true;
|
||||||
dragStartX = e.clientX;
|
dragStartX = e.clientX;
|
||||||
dragStartY = e.clientY;
|
dragStartY = e.clientY;
|
||||||
lastOffsetX = offsetX;
|
lastOffsetX = offsetX;
|
||||||
lastOffsetY = offsetY;
|
lastOffsetY = offsetY;
|
||||||
canvas.style.cursor = 'grabbing';
|
canvas.style.cursor = 'grabbing';
|
||||||
|
}
|
||||||
});
|
});
|
||||||
window.addEventListener('mousemove', (e) => {
|
window.addEventListener('mousemove', (e) => {
|
||||||
if (!isDragging) return;
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const x = (e.clientX - rect.left - offsetX) / scale;
|
||||||
|
const y = (e.clientY - rect.top - offsetY) / scale;
|
||||||
|
if ((mode === 'mark' || ctrlPressed) && markStart) {
|
||||||
|
tempMark = { x1: markStart.x1, y1: markStart.y1, x2: x, y2: y, color: markColor, lineWidth: markLineWidth };
|
||||||
|
draw();
|
||||||
|
} 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);
|
||||||
draw();
|
draw();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
window.addEventListener('mouseup', () => {
|
window.addEventListener('mouseup', (e) => {
|
||||||
|
if ((mode === 'mark' || ctrlPressed) && markStart && tempMark) {
|
||||||
|
if (Math.abs(tempMark.x2 - tempMark.x1) >= 2 / scale && Math.abs(tempMark.y2 - tempMark.y1) >= 2 / scale) {
|
||||||
|
pushUndo();
|
||||||
|
marks.push({ ...tempMark });
|
||||||
|
}
|
||||||
|
markStart = null;
|
||||||
|
tempMark = null;
|
||||||
|
if (mode === 'mark') setMode('move');
|
||||||
|
draw();
|
||||||
|
} else if (mode === 'move' && isDragging) {
|
||||||
isDragging = false;
|
isDragging = false;
|
||||||
canvas.style.cursor = 'grab';
|
canvas.style.cursor = 'grab';
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let undoStack = [];
|
||||||
|
function pushUndo() {
|
||||||
|
undoStack.push(JSON.stringify(marks));
|
||||||
|
if (undoStack.length > 100) undoStack.shift();
|
||||||
|
}
|
||||||
|
function undo() {
|
||||||
|
if (undoStack.length > 0) {
|
||||||
|
marks = JSON.parse(undoStack.pop());
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 初期リサイズ
|
// 初期リサイズ
|
||||||
resizeCanvas();
|
resizeCanvas();
|
||||||
</script>
|
</script>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user