@wasao/kagemusha 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/commands/capture.d.ts +1 -0
- package/dist/commands/capture.d.ts.map +1 -1
- package/dist/commands/capture.js +14 -3
- package/dist/commands/capture.js.map +1 -1
- package/dist/commands/edit.d.ts +6 -0
- package/dist/commands/edit.d.ts.map +1 -0
- package/dist/commands/edit.js +100 -0
- package/dist/commands/edit.js.map +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +14 -16
- package/dist/commands/init.js.map +1 -1
- package/dist/editor/editor/editor.html +313 -0
- package/dist/editor/editor/inject.ts +385 -0
- package/dist/editor/editor.html +338 -0
- package/dist/editor/inject-script.cjs +398 -0
- package/dist/editor/inject-script.cjs.map +1 -0
- package/dist/editor/inject-script.d.cts +2 -0
- package/dist/editor/inject-script.d.cts.map +1 -0
- package/dist/editor/inject-script.d.ts +2 -0
- package/dist/editor/inject-script.d.ts.map +1 -0
- package/dist/editor/inject-script.js +394 -0
- package/dist/editor/inject-script.js.map +1 -0
- package/dist/editor/inject.d.ts +2 -0
- package/dist/editor/inject.d.ts.map +1 -0
- package/dist/editor/inject.js +385 -0
- package/dist/editor/inject.js.map +1 -0
- package/dist/index.js +6 -5
- package/dist/index.js.map +1 -1
- package/dist/lib/annotate.js +1 -1
- package/dist/lib/annotate.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>Kagemusha Annotation Editor</title>
|
|
6
|
+
<style>
|
|
7
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
8
|
+
body { background: #1a1a2e; font-family: -apple-system, BlinkMacSystemFont, sans-serif; overflow: hidden; }
|
|
9
|
+
|
|
10
|
+
.toolbar {
|
|
11
|
+
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
|
|
12
|
+
background: #16213e; padding: 8px 16px; display: flex; align-items: center; gap: 12px;
|
|
13
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
|
14
|
+
}
|
|
15
|
+
.toolbar button {
|
|
16
|
+
padding: 6px 14px; border: 1px solid #444; border-radius: 6px;
|
|
17
|
+
background: #1a1a2e; color: #fff; font-size: 13px; cursor: pointer;
|
|
18
|
+
}
|
|
19
|
+
.toolbar button:hover { background: #2a2a4e; }
|
|
20
|
+
.toolbar button.active { background: #6366f1; border-color: #6366f1; }
|
|
21
|
+
.toolbar .separator { width: 1px; height: 24px; background: #444; }
|
|
22
|
+
.toolbar .title { color: #888; font-size: 13px; margin-right: 8px; }
|
|
23
|
+
.toolbar .save-btn { background: #22c55e; border-color: #22c55e; font-weight: 600; margin-left: auto; }
|
|
24
|
+
.toolbar .save-btn:hover { background: #16a34a; }
|
|
25
|
+
|
|
26
|
+
.canvas-area {
|
|
27
|
+
margin-top: 48px; display: flex; justify-content: center; padding: 20px;
|
|
28
|
+
height: calc(100vh - 48px); overflow: auto;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.editor-container {
|
|
32
|
+
position: relative; display: inline-block; cursor: crosshair;
|
|
33
|
+
}
|
|
34
|
+
.editor-container img { display: block; max-width: 100%; }
|
|
35
|
+
|
|
36
|
+
.annotation {
|
|
37
|
+
position: absolute; cursor: move;
|
|
38
|
+
}
|
|
39
|
+
.annotation.selected { outline: 2px dashed #6366f1; outline-offset: 2px; }
|
|
40
|
+
.annotation .handle {
|
|
41
|
+
position: absolute; width: 8px; height: 8px; background: #6366f1;
|
|
42
|
+
border: 1px solid #fff; border-radius: 2px;
|
|
43
|
+
}
|
|
44
|
+
.annotation .handle-br { bottom: -4px; right: -4px; cursor: se-resize; }
|
|
45
|
+
.annotation .handle-bl { bottom: -4px; left: -4px; cursor: sw-resize; }
|
|
46
|
+
.annotation .handle-tr { top: -4px; right: -4px; cursor: ne-resize; }
|
|
47
|
+
.annotation .handle-tl { top: -4px; left: -4px; cursor: nw-resize; }
|
|
48
|
+
|
|
49
|
+
.label-input {
|
|
50
|
+
background: none; border: none; color: inherit; font: inherit;
|
|
51
|
+
width: 100%; outline: none;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.hint {
|
|
55
|
+
position: fixed; bottom: 16px; left: 50%; transform: translateX(-50%);
|
|
56
|
+
color: #666; font-size: 12px;
|
|
57
|
+
}
|
|
58
|
+
</style>
|
|
59
|
+
</head>
|
|
60
|
+
<body>
|
|
61
|
+
|
|
62
|
+
<div class="toolbar">
|
|
63
|
+
<span class="title">🥷 Annotation Editor</span>
|
|
64
|
+
<button id="tool-rect" class="active" onclick="setTool('rect')">▭ Rectangle</button>
|
|
65
|
+
<button id="tool-arrow" onclick="setTool('arrow')">→ Arrow</button>
|
|
66
|
+
<button id="tool-label" onclick="setTool('label')">T Label</button>
|
|
67
|
+
<div class="separator"></div>
|
|
68
|
+
<button onclick="deleteSelected()">🗑 Delete</button>
|
|
69
|
+
<button class="save-btn" onclick="save()">💾 Save</button>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<div class="canvas-area">
|
|
73
|
+
<div class="editor-container" id="editor">
|
|
74
|
+
<img id="screenshot" />
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<div class="hint">Click and drag to add annotations. Click to select. Press Delete to remove.</div>
|
|
79
|
+
|
|
80
|
+
<script>
|
|
81
|
+
let tool = 'rect';
|
|
82
|
+
let annotations = [];
|
|
83
|
+
let selectedId = null;
|
|
84
|
+
let dragState = null;
|
|
85
|
+
let nextId = 1;
|
|
86
|
+
|
|
87
|
+
const editor = document.getElementById('editor');
|
|
88
|
+
const img = document.getElementById('screenshot');
|
|
89
|
+
|
|
90
|
+
// Set by Playwright
|
|
91
|
+
window.__SCREENSHOT_PATH__ = '';
|
|
92
|
+
window.__EXISTING_DECORATIONS__ = [];
|
|
93
|
+
|
|
94
|
+
window.initEditor = (screenshotDataUrl, existingDecorations) => {
|
|
95
|
+
img.src = screenshotDataUrl;
|
|
96
|
+
if (existingDecorations) {
|
|
97
|
+
existingDecorations.forEach(d => {
|
|
98
|
+
addAnnotationFromDecoration(d);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
function setTool(t) {
|
|
104
|
+
tool = t;
|
|
105
|
+
document.querySelectorAll('.toolbar button').forEach(b => b.classList.remove('active'));
|
|
106
|
+
document.getElementById('tool-' + t)?.classList.add('active');
|
|
107
|
+
deselectAll();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function deselectAll() {
|
|
111
|
+
selectedId = null;
|
|
112
|
+
document.querySelectorAll('.annotation').forEach(el => el.classList.remove('selected'));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function deleteSelected() {
|
|
116
|
+
if (!selectedId) return;
|
|
117
|
+
const el = document.querySelector(`[data-id="${selectedId}"]`);
|
|
118
|
+
if (el) el.remove();
|
|
119
|
+
annotations = annotations.filter(a => a.id !== selectedId);
|
|
120
|
+
selectedId = null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
document.addEventListener('keydown', e => {
|
|
124
|
+
if (e.key === 'Delete' || e.key === 'Backspace') {
|
|
125
|
+
if (document.activeElement.classList?.contains('label-input')) return;
|
|
126
|
+
deleteSelected();
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
editor.addEventListener('mousedown', e => {
|
|
131
|
+
if (e.target.closest('.annotation')) return;
|
|
132
|
+
const rect = editor.getBoundingClientRect();
|
|
133
|
+
const x = e.clientX - rect.left;
|
|
134
|
+
const y = e.clientY - rect.top;
|
|
135
|
+
deselectAll();
|
|
136
|
+
|
|
137
|
+
if (tool === 'rect') {
|
|
138
|
+
dragState = { type: 'create-rect', startX: x, startY: y };
|
|
139
|
+
const el = createRectElement(nextId, x, y, 0, 0);
|
|
140
|
+
editor.appendChild(el);
|
|
141
|
+
dragState.element = el;
|
|
142
|
+
dragState.id = nextId++;
|
|
143
|
+
} else if (tool === 'arrow') {
|
|
144
|
+
dragState = { type: 'create-arrow', startX: x, startY: y };
|
|
145
|
+
const el = createArrowElement(nextId, x, y, x, y);
|
|
146
|
+
editor.appendChild(el);
|
|
147
|
+
dragState.element = el;
|
|
148
|
+
dragState.id = nextId++;
|
|
149
|
+
} else if (tool === 'label') {
|
|
150
|
+
const id = nextId++;
|
|
151
|
+
const annotation = { id, type: 'label', x, y, text: 'Label' };
|
|
152
|
+
annotations.push(annotation);
|
|
153
|
+
const el = createLabelElement(id, x, y, 'Label');
|
|
154
|
+
editor.appendChild(el);
|
|
155
|
+
selectedId = id;
|
|
156
|
+
el.classList.add('selected');
|
|
157
|
+
el.querySelector('.label-input').focus();
|
|
158
|
+
el.querySelector('.label-input').select();
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
document.addEventListener('mousemove', e => {
|
|
163
|
+
if (!dragState) return;
|
|
164
|
+
const rect = editor.getBoundingClientRect();
|
|
165
|
+
const x = e.clientX - rect.left;
|
|
166
|
+
const y = e.clientY - rect.top;
|
|
167
|
+
|
|
168
|
+
if (dragState.type === 'create-rect') {
|
|
169
|
+
const el = dragState.element;
|
|
170
|
+
const w = x - dragState.startX;
|
|
171
|
+
const h = y - dragState.startY;
|
|
172
|
+
el.style.left = (w > 0 ? dragState.startX : x) + 'px';
|
|
173
|
+
el.style.top = (h > 0 ? dragState.startY : y) + 'px';
|
|
174
|
+
el.style.width = Math.abs(w) + 'px';
|
|
175
|
+
el.style.height = Math.abs(h) + 'px';
|
|
176
|
+
} else if (dragState.type === 'create-arrow') {
|
|
177
|
+
updateArrowElement(dragState.element, dragState.startX, dragState.startY, x, y);
|
|
178
|
+
} else if (dragState.type === 'move') {
|
|
179
|
+
const a = annotations.find(a => a.id === dragState.id);
|
|
180
|
+
if (a) {
|
|
181
|
+
if (a.type === 'rect' || a.type === 'label') {
|
|
182
|
+
a.x = x - dragState.offsetX;
|
|
183
|
+
a.y = y - dragState.offsetY;
|
|
184
|
+
dragState.element.style.left = a.x + 'px';
|
|
185
|
+
dragState.element.style.top = a.y + 'px';
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
document.addEventListener('mouseup', e => {
|
|
192
|
+
if (!dragState) return;
|
|
193
|
+
const rect = editor.getBoundingClientRect();
|
|
194
|
+
const x = e.clientX - rect.left;
|
|
195
|
+
const y = e.clientY - rect.top;
|
|
196
|
+
|
|
197
|
+
if (dragState.type === 'create-rect') {
|
|
198
|
+
const w = Math.abs(x - dragState.startX);
|
|
199
|
+
const h = Math.abs(y - dragState.startY);
|
|
200
|
+
if (w < 5 && h < 5) {
|
|
201
|
+
dragState.element.remove();
|
|
202
|
+
} else {
|
|
203
|
+
const annotation = {
|
|
204
|
+
id: dragState.id, type: 'rect',
|
|
205
|
+
x: Math.min(dragState.startX, x), y: Math.min(dragState.startY, y),
|
|
206
|
+
width: w, height: h
|
|
207
|
+
};
|
|
208
|
+
annotations.push(annotation);
|
|
209
|
+
dragState.element.dataset.id = dragState.id;
|
|
210
|
+
selectedId = dragState.id;
|
|
211
|
+
dragState.element.classList.add('selected');
|
|
212
|
+
}
|
|
213
|
+
} else if (dragState.type === 'create-arrow') {
|
|
214
|
+
const dist = Math.hypot(x - dragState.startX, y - dragState.startY);
|
|
215
|
+
if (dist < 5) {
|
|
216
|
+
dragState.element.remove();
|
|
217
|
+
} else {
|
|
218
|
+
const annotation = {
|
|
219
|
+
id: dragState.id, type: 'arrow',
|
|
220
|
+
fromX: dragState.startX, fromY: dragState.startY,
|
|
221
|
+
toX: x, toY: y
|
|
222
|
+
};
|
|
223
|
+
annotations.push(annotation);
|
|
224
|
+
dragState.element.dataset.id = dragState.id;
|
|
225
|
+
selectedId = dragState.id;
|
|
226
|
+
dragState.element.classList.add('selected');
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
dragState = null;
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
function createRectElement(id, x, y, w, h) {
|
|
233
|
+
const el = document.createElement('div');
|
|
234
|
+
el.className = 'annotation';
|
|
235
|
+
el.dataset.id = id;
|
|
236
|
+
el.style.cssText = `left:${x}px;top:${y}px;width:${w}px;height:${h}px;border:2px solid #FF0000;border-radius:4px;`;
|
|
237
|
+
el.innerHTML = '<div class="handle handle-br"></div>';
|
|
238
|
+
el.addEventListener('mousedown', e => {
|
|
239
|
+
e.stopPropagation();
|
|
240
|
+
selectedId = parseInt(el.dataset.id);
|
|
241
|
+
deselectAll();
|
|
242
|
+
el.classList.add('selected');
|
|
243
|
+
selectedId = parseInt(el.dataset.id);
|
|
244
|
+
const a = annotations.find(a => a.id === selectedId);
|
|
245
|
+
if (a) {
|
|
246
|
+
const rect = editor.getBoundingClientRect();
|
|
247
|
+
dragState = { type: 'move', id: selectedId, element: el, offsetX: e.clientX - rect.left - a.x, offsetY: e.clientY - rect.top - a.y };
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
return el;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function createArrowElement(id, x1, y1, x2, y2) {
|
|
254
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
255
|
+
svg.classList.add('annotation');
|
|
256
|
+
svg.dataset.id = id;
|
|
257
|
+
svg.style.cssText = 'position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none;';
|
|
258
|
+
svg.innerHTML = `
|
|
259
|
+
<defs><marker id="ah-${id}" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto" fill="#FF0000">
|
|
260
|
+
<polygon points="0 0, 10 3.5, 0 7"/></marker></defs>
|
|
261
|
+
<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="#FF0000" stroke-width="2" marker-end="url(#ah-${id})" style="pointer-events:stroke;cursor:move;" />
|
|
262
|
+
`;
|
|
263
|
+
svg.querySelector('line').addEventListener('mousedown', e => {
|
|
264
|
+
e.stopPropagation();
|
|
265
|
+
deselectAll();
|
|
266
|
+
selectedId = parseInt(svg.dataset.id);
|
|
267
|
+
svg.classList.add('selected');
|
|
268
|
+
});
|
|
269
|
+
return svg;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function updateArrowElement(svg, x1, y1, x2, y2) {
|
|
273
|
+
const line = svg.querySelector('line');
|
|
274
|
+
line.setAttribute('x1', x1);
|
|
275
|
+
line.setAttribute('y1', y1);
|
|
276
|
+
line.setAttribute('x2', x2);
|
|
277
|
+
line.setAttribute('y2', y2);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function createLabelElement(id, x, y, text) {
|
|
281
|
+
const el = document.createElement('div');
|
|
282
|
+
el.className = 'annotation';
|
|
283
|
+
el.dataset.id = id;
|
|
284
|
+
el.style.cssText = `left:${x}px;top:${y}px;padding:4px 8px;background:#fff;border:1px solid #FF0000;border-radius:4px;color:#FF0000;font-size:14px;min-width:40px;`;
|
|
285
|
+
el.innerHTML = `<input class="label-input" value="${text}" style="color:#FF0000;font-size:14px;" />`;
|
|
286
|
+
el.querySelector('.label-input').addEventListener('input', e => {
|
|
287
|
+
const a = annotations.find(a => a.id === parseInt(el.dataset.id));
|
|
288
|
+
if (a) a.text = e.target.value;
|
|
289
|
+
});
|
|
290
|
+
el.addEventListener('mousedown', e => {
|
|
291
|
+
if (e.target.classList.contains('label-input')) return;
|
|
292
|
+
e.stopPropagation();
|
|
293
|
+
deselectAll();
|
|
294
|
+
selectedId = parseInt(el.dataset.id);
|
|
295
|
+
el.classList.add('selected');
|
|
296
|
+
const a = annotations.find(a => a.id === selectedId);
|
|
297
|
+
if (a) {
|
|
298
|
+
const rect = editor.getBoundingClientRect();
|
|
299
|
+
dragState = { type: 'move', id: selectedId, element: el, offsetX: e.clientX - rect.left - a.x, offsetY: e.clientY - rect.top - a.y };
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
return el;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function addAnnotationFromDecoration(d) {
|
|
306
|
+
const id = nextId++;
|
|
307
|
+
if (d.type === 'rect' && d.target && 'x' in d.target) {
|
|
308
|
+
const a = { id, type: 'rect', x: d.target.x, y: d.target.y, width: d.target.width, height: d.target.height };
|
|
309
|
+
annotations.push(a);
|
|
310
|
+
editor.appendChild(createRectElement(id, a.x, a.y, a.width, a.height));
|
|
311
|
+
} else if (d.type === 'arrow' && 'x' in d.from && 'x' in d.to) {
|
|
312
|
+
const a = { id, type: 'arrow', fromX: d.from.x, fromY: d.from.y, toX: d.to.x, toY: d.to.y };
|
|
313
|
+
annotations.push(a);
|
|
314
|
+
editor.appendChild(createArrowElement(id, a.fromX, a.fromY, a.toX, a.toY));
|
|
315
|
+
} else if (d.type === 'label' && 'x' in d.position) {
|
|
316
|
+
const a = { id, type: 'label', x: d.position.x, y: d.position.y, text: d.text };
|
|
317
|
+
annotations.push(a);
|
|
318
|
+
editor.appendChild(createLabelElement(id, a.x, a.y, a.text));
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function save() {
|
|
323
|
+
const decorations = annotations.map(a => {
|
|
324
|
+
if (a.type === 'rect') {
|
|
325
|
+
return { type: 'rect', target: { x: Math.round(a.x), y: Math.round(a.y), width: Math.round(a.width), height: Math.round(a.height) }, style: { color: '#FF0000', strokeWidth: 2 } };
|
|
326
|
+
} else if (a.type === 'arrow') {
|
|
327
|
+
return { type: 'arrow', from: { x: Math.round(a.fromX), y: Math.round(a.fromY) }, to: { x: Math.round(a.toX), y: Math.round(a.toY) }, style: { color: '#FF0000', strokeWidth: 2 } };
|
|
328
|
+
} else if (a.type === 'label') {
|
|
329
|
+
return { type: 'label', text: a.text, position: { x: Math.round(a.x), y: Math.round(a.y) }, style: { fontSize: 14, color: '#FF0000', background: '#FFFFFF' } };
|
|
330
|
+
}
|
|
331
|
+
}).filter(Boolean);
|
|
332
|
+
|
|
333
|
+
window.__SAVED_DECORATIONS__ = decorations;
|
|
334
|
+
document.title = 'SAVED';
|
|
335
|
+
}
|
|
336
|
+
</script>
|
|
337
|
+
</body>
|
|
338
|
+
</html>
|