@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,385 @@
1
+ // This script is injected into the target page to provide annotation editing.
2
+ // It adds a toolbar and SVG overlay layer on top of the actual page.
3
+
4
+ export const EDITOR_SCRIPT = `
5
+ (function() {
6
+ let tool = 'rect';
7
+ let annotations = [];
8
+ let selectedId = null;
9
+ let dragState = null;
10
+ let nextId = 1;
11
+
12
+ // --- TOOLBAR ---
13
+ const toolbar = document.createElement('div');
14
+ toolbar.id = 'kagemusha-toolbar';
15
+ toolbar.innerHTML = \`
16
+ <style>
17
+ #kagemusha-toolbar {
18
+ position: fixed; top: 0; left: 0; right: 0; z-index: 999999;
19
+ background: #16213e; padding: 8px 16px; display: flex; align-items: center; gap: 12px;
20
+ box-shadow: 0 2px 8px rgba(0,0,0,0.3); font-family: -apple-system, sans-serif;
21
+ }
22
+ #kagemusha-toolbar button {
23
+ padding: 6px 14px; border: 1px solid #444; border-radius: 6px;
24
+ background: #1a1a2e; color: #fff; font-size: 13px; cursor: pointer;
25
+ }
26
+ #kagemusha-toolbar button:hover { background: #2a2a4e; }
27
+ #kagemusha-toolbar button.active { background: #6366f1; border-color: #6366f1; }
28
+ #kagemusha-toolbar .sep { width: 1px; height: 24px; background: #444; }
29
+ #kagemusha-toolbar .title { color: #888; font-size: 13px; }
30
+ #kagemusha-toolbar .save-btn { background: #22c55e; border-color: #22c55e; font-weight: 600; margin-left: auto; }
31
+ #kagemusha-toolbar .save-btn:hover { background: #16a34a; }
32
+ #kagemusha-svg-layer {
33
+ position: absolute; top: 0; left: 0; width: 100%;
34
+ z-index: 999998; pointer-events: none;
35
+ }
36
+ #kagemusha-svg-layer.drawing { pointer-events: auto; cursor: crosshair; }
37
+ #kagemusha-svg-layer .annotation { pointer-events: auto; cursor: move; }
38
+ #kagemusha-svg-layer .annotation.selected { filter: drop-shadow(0 0 3px #6366f1); }
39
+ .kagemusha-hint {
40
+ position: fixed; bottom: 16px; left: 50%; transform: translateX(-50%);
41
+ color: #fff; background: rgba(0,0,0,0.7); padding: 6px 16px; border-radius: 8px;
42
+ font-size: 12px; z-index: 999999; font-family: -apple-system, sans-serif;
43
+ }
44
+ </style>
45
+ <span class="title">🥷 Annotation Editor</span>
46
+ <button id="kg-tool-rect" class="active">▭ Rectangle</button>
47
+ <button id="kg-tool-arrow">→ Arrow</button>
48
+ <button id="kg-tool-label">T Label</button>
49
+ <div class="sep"></div>
50
+ <button id="kg-delete">🗑 Delete</button>
51
+ <button class="save-btn" id="kg-save">💾 Save</button>
52
+ \`;
53
+ document.body.appendChild(toolbar);
54
+
55
+ // Shift page content down so toolbar doesn't overlap
56
+ document.body.style.paddingTop = '48px';
57
+
58
+ // Hint
59
+ const hint = document.createElement('div');
60
+ hint.className = 'kagemusha-hint';
61
+ hint.textContent = 'Click and drag to add annotations. Click to select. Press Delete to remove.';
62
+ document.body.appendChild(hint);
63
+
64
+ // --- SVG LAYER ---
65
+ const svgNS = 'http://www.w3.org/2000/svg';
66
+ const svg = document.createElementNS(svgNS, 'svg');
67
+ svg.id = 'kagemusha-svg-layer';
68
+ svg.classList.add('drawing');
69
+ document.body.appendChild(svg);
70
+
71
+ function updateSvgSize() {
72
+ svg.setAttribute('width', String(window.innerWidth));
73
+ svg.setAttribute('height', String(document.documentElement.scrollHeight));
74
+ }
75
+ updateSvgSize();
76
+ window.addEventListener('resize', updateSvgSize);
77
+
78
+ // Arrowhead marker
79
+ const defs = document.createElementNS(svgNS, 'defs');
80
+ defs.innerHTML = '<marker id="kg-arrowhead" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto" fill="#FF0000"><polygon points="0 0, 10 3.5, 0 7"/></marker>';
81
+ svg.appendChild(defs);
82
+
83
+ // --- TOOLS ---
84
+ function setTool(t) {
85
+ tool = t;
86
+ document.querySelectorAll('#kagemusha-toolbar button').forEach(b => b.classList.remove('active'));
87
+ const btn = document.getElementById('kg-tool-' + t);
88
+ if (btn) btn.classList.add('active');
89
+ svg.classList.toggle('drawing', true);
90
+ deselectAll();
91
+ }
92
+
93
+ document.getElementById('kg-tool-rect').addEventListener('click', () => setTool('rect'));
94
+ document.getElementById('kg-tool-arrow').addEventListener('click', () => setTool('arrow'));
95
+ document.getElementById('kg-tool-label').addEventListener('click', () => setTool('label'));
96
+ document.getElementById('kg-delete').addEventListener('click', deleteSelected);
97
+ document.getElementById('kg-save').addEventListener('click', save);
98
+
99
+ function deselectAll() {
100
+ selectedId = null;
101
+ svg.querySelectorAll('.annotation').forEach(el => el.classList.remove('selected'));
102
+ }
103
+
104
+ function selectEl(el) {
105
+ deselectAll();
106
+ selectedId = el.dataset.id;
107
+ el.classList.add('selected');
108
+ }
109
+
110
+ function deleteSelected() {
111
+ if (!selectedId) return;
112
+ const el = svg.querySelector('[data-id="' + selectedId + '"]');
113
+ if (el) el.remove();
114
+ annotations = annotations.filter(a => a.id !== selectedId);
115
+ selectedId = null;
116
+ }
117
+
118
+ document.addEventListener('keydown', e => {
119
+ if (e.key === 'Delete' || e.key === 'Backspace') {
120
+ if (document.activeElement.tagName === 'INPUT') return;
121
+ deleteSelected();
122
+ }
123
+ });
124
+
125
+ // --- MOUSE HANDLING ---
126
+ function getPos(e) {
127
+ return { x: e.pageX, y: e.pageY };
128
+ }
129
+
130
+ svg.addEventListener('mousedown', e => {
131
+ if (e.target.closest('.annotation')) return;
132
+ const p = getPos(e);
133
+ deselectAll();
134
+
135
+ if (tool === 'rect') {
136
+ const id = 'a' + nextId++;
137
+ const rect = document.createElementNS(svgNS, 'rect');
138
+ rect.setAttribute('x', p.x);
139
+ rect.setAttribute('y', p.y);
140
+ rect.setAttribute('width', '0');
141
+ rect.setAttribute('height', '0');
142
+ rect.setAttribute('fill', 'none');
143
+ rect.setAttribute('stroke', '#FF0000');
144
+ rect.setAttribute('stroke-width', '3');
145
+ rect.setAttribute('rx', '4');
146
+ rect.classList.add('annotation');
147
+ rect.dataset.id = id;
148
+ svg.appendChild(rect);
149
+ dragState = { type: 'create-rect', id, el: rect, sx: p.x, sy: p.y };
150
+ } else if (tool === 'arrow') {
151
+ const id = 'a' + nextId++;
152
+ const line = document.createElementNS(svgNS, 'line');
153
+ line.setAttribute('x1', p.x);
154
+ line.setAttribute('y1', p.y);
155
+ line.setAttribute('x2', p.x);
156
+ line.setAttribute('y2', p.y);
157
+ line.setAttribute('stroke', '#FF0000');
158
+ line.setAttribute('stroke-width', '3');
159
+ line.setAttribute('marker-end', 'url(#kg-arrowhead)');
160
+ line.classList.add('annotation');
161
+ line.dataset.id = id;
162
+ svg.appendChild(line);
163
+ dragState = { type: 'create-arrow', id, el: line, sx: p.x, sy: p.y };
164
+ } else if (tool === 'label') {
165
+ const id = 'a' + nextId++;
166
+ // Create inline input on the page
167
+ const input = document.createElement('input');
168
+ input.type = 'text';
169
+ input.value = '';
170
+ input.placeholder = 'Type label...';
171
+ input.style.cssText = 'position:fixed;z-index:9999999;padding:4px 8px;background:#fff;border:none;border-radius:4px;color:#FF0000;font-size:14px;font-family:-apple-system,sans-serif;outline:2px solid #6366f1;min-width:80px;box-shadow:0 2px 8px rgba(0,0,0,0.2);';
172
+ input.style.left = (e.clientX) + 'px';
173
+ input.style.top = (e.clientY) + 'px';
174
+ document.body.appendChild(input);
175
+ svg.classList.remove('drawing');
176
+ setTimeout(() => input.focus(), 50);
177
+
178
+ function finishLabel() {
179
+ const text = input.value.trim();
180
+ input.remove();
181
+ svg.classList.add('drawing');
182
+ if (!text) return;
183
+
184
+ const g = document.createElementNS(svgNS, 'g');
185
+ g.classList.add('annotation');
186
+ g.dataset.id = id;
187
+ const bg = document.createElementNS(svgNS, 'rect');
188
+ const txt = document.createElementNS(svgNS, 'text');
189
+ txt.textContent = text;
190
+ txt.setAttribute('x', String(p.x + 6));
191
+ txt.setAttribute('y', String(p.y + 16));
192
+ txt.setAttribute('fill', '#FF0000');
193
+ txt.setAttribute('font-size', '14');
194
+ txt.setAttribute('font-family', '-apple-system, sans-serif');
195
+ const tw = text.length * 9 + 12;
196
+ bg.setAttribute('x', String(p.x));
197
+ bg.setAttribute('y', String(p.y));
198
+ bg.setAttribute('width', String(tw));
199
+ bg.setAttribute('height', '24');
200
+ bg.setAttribute('fill', '#FFFFFF');
201
+ bg.setAttribute('rx', '4');
202
+ g.appendChild(bg);
203
+ g.appendChild(txt);
204
+ svg.appendChild(g);
205
+ annotations.push({ id, type: 'label', x: p.x, y: p.y, text });
206
+ selectEl(g);
207
+ g.addEventListener('mousedown', e => startMove(e, id));
208
+ }
209
+
210
+ input.addEventListener('keydown', e => {
211
+ if (e.key === 'Enter') finishLabel();
212
+ if (e.key === 'Escape') { input.remove(); svg.classList.add('drawing'); }
213
+ });
214
+ input.addEventListener('blur', finishLabel);
215
+ }
216
+ });
217
+
218
+ document.addEventListener('mousemove', e => {
219
+ if (!dragState) return;
220
+ const p = getPos(e);
221
+
222
+ if (dragState.type === 'create-rect') {
223
+ const el = dragState.el;
224
+ const x = Math.min(dragState.sx, p.x);
225
+ const y = Math.min(dragState.sy, p.y);
226
+ el.setAttribute('x', x);
227
+ el.setAttribute('y', y);
228
+ el.setAttribute('width', Math.abs(p.x - dragState.sx));
229
+ el.setAttribute('height', Math.abs(p.y - dragState.sy));
230
+ } else if (dragState.type === 'create-arrow') {
231
+ dragState.el.setAttribute('x2', p.x);
232
+ dragState.el.setAttribute('y2', p.y);
233
+ } else if (dragState.type === 'move') {
234
+ const a = annotations.find(a => a.id === dragState.id);
235
+ if (!a) return;
236
+ const dx = p.x - dragState.lastX;
237
+ const dy = p.y - dragState.lastY;
238
+ dragState.lastX = p.x;
239
+ dragState.lastY = p.y;
240
+
241
+ if (a.type === 'rect') {
242
+ a.x += dx; a.y += dy;
243
+ dragState.el.setAttribute('x', a.x);
244
+ dragState.el.setAttribute('y', a.y);
245
+ } else if (a.type === 'arrow') {
246
+ a.fromX += dx; a.fromY += dy; a.toX += dx; a.toY += dy;
247
+ dragState.el.setAttribute('x1', a.fromX);
248
+ dragState.el.setAttribute('y1', a.fromY);
249
+ dragState.el.setAttribute('x2', a.toX);
250
+ dragState.el.setAttribute('y2', a.toY);
251
+ } else if (a.type === 'label') {
252
+ a.x += dx; a.y += dy;
253
+ const bg = dragState.el.querySelector('rect');
254
+ const txt = dragState.el.querySelector('text');
255
+ bg.setAttribute('x', a.x);
256
+ bg.setAttribute('y', a.y);
257
+ txt.setAttribute('x', a.x + 6);
258
+ txt.setAttribute('y', a.y + 16);
259
+ }
260
+ }
261
+ });
262
+
263
+ document.addEventListener('mouseup', e => {
264
+ if (!dragState) return;
265
+ const p = getPos(e);
266
+
267
+ if (dragState.type === 'create-rect') {
268
+ const w = Math.abs(p.x - dragState.sx);
269
+ const h = Math.abs(p.y - dragState.sy);
270
+ if (w < 5 && h < 5) { dragState.el.remove(); }
271
+ else {
272
+ const a = { id: dragState.id, type: 'rect', x: Math.min(dragState.sx, p.x), y: Math.min(dragState.sy, p.y), width: w, height: h };
273
+ annotations.push(a);
274
+ selectEl(dragState.el);
275
+ dragState.el.addEventListener('mousedown', e => startMove(e, dragState.id));
276
+ }
277
+ } else if (dragState.type === 'create-arrow') {
278
+ const dist = Math.hypot(p.x - dragState.sx, p.y - dragState.sy);
279
+ if (dist < 5) { dragState.el.remove(); }
280
+ else {
281
+ const a = { id: dragState.id, type: 'arrow', fromX: dragState.sx, fromY: dragState.sy, toX: p.x, toY: p.y };
282
+ annotations.push(a);
283
+ selectEl(dragState.el);
284
+ dragState.el.addEventListener('mousedown', e => startMove(e, dragState.id));
285
+ }
286
+ }
287
+ dragState = null;
288
+ });
289
+
290
+ function startMove(e, id) {
291
+ e.stopPropagation();
292
+ const el = svg.querySelector('[data-id="' + id + '"]');
293
+ selectEl(el);
294
+ const p = getPos(e);
295
+ dragState = { type: 'move', id, el, lastX: p.x, lastY: p.y };
296
+ }
297
+
298
+ // --- LOAD EXISTING ---
299
+ window.__kagemusha_loadAnnotations = function(decorations) {
300
+ const pad = 48; // toolbar height to add to Y
301
+ const dpr = window.__kagemusha_dpr || 1;
302
+ decorations.forEach(d => {
303
+ const id = 'a' + nextId++;
304
+ if (d.type === 'rect' && d.target && 'x' in d.target) {
305
+ const rx = d.target.x / dpr, ry = d.target.y / dpr + pad;
306
+ const rect = document.createElementNS(svgNS, 'rect');
307
+ const rw = d.target.width / dpr, rh = d.target.height / dpr;
308
+ rect.setAttribute('x', rx);
309
+ rect.setAttribute('y', ry);
310
+ rect.setAttribute('width', rw);
311
+ rect.setAttribute('height', rh);
312
+ rect.setAttribute('fill', 'none');
313
+ rect.setAttribute('stroke', d.style?.color || '#FF0000');
314
+ rect.setAttribute('stroke-width', '3');
315
+ rect.setAttribute('rx', '4');
316
+ rect.classList.add('annotation');
317
+ rect.dataset.id = id;
318
+ svg.appendChild(rect);
319
+ annotations.push({ id, type: 'rect', x: rx, y: ry, width: rw, height: rh });
320
+ rect.addEventListener('mousedown', e => startMove(e, id));
321
+ } else if (d.type === 'arrow' && d.from && 'x' in d.from) {
322
+ const line = document.createElementNS(svgNS, 'line');
323
+ const ax1 = d.from.x / dpr, ay1 = d.from.y / dpr + pad;
324
+ const ax2 = d.to.x / dpr, ay2 = d.to.y / dpr + pad;
325
+ line.setAttribute('x1', ax1);
326
+ line.setAttribute('y1', ay1);
327
+ line.setAttribute('x2', ax2);
328
+ line.setAttribute('y2', ay2);
329
+ line.setAttribute('stroke', d.style?.color || '#FF0000');
330
+ line.setAttribute('stroke-width', '3');
331
+ line.setAttribute('marker-end', 'url(#kg-arrowhead)');
332
+ line.classList.add('annotation');
333
+ line.dataset.id = id;
334
+ svg.appendChild(line);
335
+ annotations.push({ id, type: 'arrow', fromX: ax1, fromY: ay1, toX: ax2, toY: ay2 });
336
+ line.addEventListener('mousedown', e => startMove(e, id));
337
+ } else if (d.type === 'label' && d.position && 'x' in d.position) {
338
+ const lx = d.position.x / dpr, ly = d.position.y / dpr + pad;
339
+ const g = document.createElementNS(svgNS, 'g');
340
+ g.classList.add('annotation');
341
+ g.dataset.id = id;
342
+ const bg = document.createElementNS(svgNS, 'rect');
343
+ const txt = document.createElementNS(svgNS, 'text');
344
+ txt.textContent = d.text;
345
+ txt.setAttribute('x', lx + 6);
346
+ txt.setAttribute('y', ly + 16);
347
+ txt.setAttribute('fill', d.style?.color || '#FF0000');
348
+ txt.setAttribute('font-size', d.style?.fontSize || '14');
349
+ txt.setAttribute('font-family', '-apple-system, sans-serif');
350
+ const tw = (d.text || '').length * 9 + 12;
351
+ bg.setAttribute('x', lx);
352
+ bg.setAttribute('y', ly);
353
+ bg.setAttribute('width', tw);
354
+ bg.setAttribute('height', '24');
355
+ bg.setAttribute('fill', d.style?.background || '#FFFFFF');
356
+ bg.setAttribute('rx', '4');
357
+ g.appendChild(bg);
358
+ g.appendChild(txt);
359
+ svg.appendChild(g);
360
+ annotations.push({ id, type: 'label', x: lx, y: ly, text: d.text });
361
+ g.addEventListener('mousedown', e => startMove(e, id));
362
+ }
363
+ });
364
+ };
365
+
366
+ // --- SAVE ---
367
+ function save() {
368
+ const pad = 48; // toolbar height to subtract from Y
369
+ const dpr = window.__kagemusha_dpr || 1;
370
+ const s = Math.round; // shorthand
371
+ const decorations = annotations.map(a => {
372
+ if (a.type === 'rect') {
373
+ return { type: 'rect', target: { x: s(a.x * dpr), y: s((a.y - pad) * dpr), width: s(a.width * dpr), height: s(a.height * dpr) }, style: { color: '#FF0000', strokeWidth: s(3 * dpr) } };
374
+ } else if (a.type === 'arrow') {
375
+ return { type: 'arrow', from: { x: s(a.fromX * dpr), y: s((a.fromY - pad) * dpr) }, to: { x: s(a.toX * dpr), y: s((a.toY - pad) * dpr) }, style: { color: '#FF0000', strokeWidth: s(3 * dpr) } };
376
+ } else if (a.type === 'label') {
377
+ return { type: 'label', text: a.text, position: { x: s(a.x * dpr), y: s((a.y - pad) * dpr) }, style: { fontSize: s(14 * dpr), color: '#FF0000', background: '#FFFFFF' } };
378
+ }
379
+ }).filter(Boolean);
380
+
381
+ window.__kagemusha_saved = decorations;
382
+ document.title = 'KAGEMUSHA_SAVED';
383
+ }
384
+ })();
385
+ `;