@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.
@@ -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>