@operato/image-marker 8.0.0-alpha.17 → 8.0.0-alpha.22
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/CHANGELOG.md +18 -0
- package/dist/src/base-image-marker.d.ts +16 -4
- package/dist/src/base-image-marker.js +96 -15
- package/dist/src/base-image-marker.js.map +1 -1
- package/dist/src/ox-image-marker-mode.js +2 -2
- package/dist/src/ox-image-marker-mode.js.map +1 -1
- package/dist/src/ox-image-marker-view.js +2 -2
- package/dist/src/ox-image-marker-view.js.map +1 -1
- package/dist/src/ox-image-marker.d.ts +9 -4
- package/dist/src/ox-image-marker.js +162 -92
- package/dist/src/ox-image-marker.js.map +1 -1
- package/dist/stories/ox-image-marker.stories.js +16 -0
- package/dist/stories/ox-image-marker.stories.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/base-image-marker.ts +114 -19
- package/src/ox-image-marker-mode.ts +2 -2
- package/src/ox-image-marker-view.ts +2 -2
- package/src/ox-image-marker.ts +181 -100
- package/stories/ox-image-marker.stories.ts +16 -0
package/src/ox-image-marker.ts
CHANGED
|
@@ -50,13 +50,27 @@ export class OxImageMarker extends BaseImageMarker {
|
|
|
50
50
|
private offsetX = 0
|
|
51
51
|
private offsetY = 0
|
|
52
52
|
|
|
53
|
+
connectedCallback() {
|
|
54
|
+
super.connectedCallback()
|
|
55
|
+
this.setAttribute('tabindex', '0')
|
|
56
|
+
this.addEventListener('focus', this.onFocus)
|
|
57
|
+
this.addEventListener('blur', this.onBlur)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
disconnectedCallback() {
|
|
61
|
+
super.disconnectedCallback()
|
|
62
|
+
this.removeEventListener('focus', this.onFocus)
|
|
63
|
+
this.removeEventListener('blur', this.onBlur)
|
|
64
|
+
}
|
|
65
|
+
|
|
53
66
|
render() {
|
|
54
67
|
return html`
|
|
55
68
|
<div class="container">
|
|
56
69
|
<svg
|
|
57
70
|
@mousedown=${this.onSvgMouseDown}
|
|
58
|
-
@
|
|
59
|
-
@
|
|
71
|
+
@touchstart=${this.onSvgTouchStart}
|
|
72
|
+
@wheel=${this.onSvgWheel}
|
|
73
|
+
@dblclick=${this.onSvgDoubleClick}
|
|
60
74
|
viewBox=${this.viewBox}
|
|
61
75
|
>
|
|
62
76
|
<image href=${this.imageUrl} @load=${this.onImageLoad}></image>
|
|
@@ -71,13 +85,8 @@ export class OxImageMarker extends BaseImageMarker {
|
|
|
71
85
|
const { mode, shapeType } = e.detail
|
|
72
86
|
this.setMode(mode, shapeType)
|
|
73
87
|
}}
|
|
74
|
-
@
|
|
75
|
-
|
|
76
|
-
this.shapes = this.shapes.filter(s => s.id !== this.selectedShape!.id)
|
|
77
|
-
this.requestUpdate()
|
|
78
|
-
}
|
|
79
|
-
}}
|
|
80
|
-
@clicked-save=${() => {
|
|
88
|
+
@tap-delete=${() => this.deleteSelectedShape()}
|
|
89
|
+
@tap-save=${() => {
|
|
81
90
|
this.dispatchEvent(
|
|
82
91
|
new CustomEvent('shapes-changed', {
|
|
83
92
|
detail: this.shapes,
|
|
@@ -91,63 +100,63 @@ export class OxImageMarker extends BaseImageMarker {
|
|
|
91
100
|
}
|
|
92
101
|
|
|
93
102
|
protected onSvgMouseDown(event: MouseEvent) {
|
|
94
|
-
const svg = this.svg
|
|
95
|
-
|
|
96
103
|
this.selectedShape = null
|
|
97
104
|
this.shapes = this.shapes.map(s => {
|
|
98
105
|
return { ...s, selected: false }
|
|
99
106
|
})
|
|
100
107
|
|
|
101
|
-
this.selectedShape = null
|
|
102
108
|
if (this.currentMode === 'move') {
|
|
103
|
-
this.
|
|
104
|
-
this.panStart = { x: event.clientX, y: event.clientY }
|
|
105
|
-
svg.classList.add('dragging')
|
|
106
|
-
svg.addEventListener('mousemove', this.onPanMouseMove)
|
|
107
|
-
svg.addEventListener('mouseup', this.onPanMouseUp)
|
|
108
|
-
} else if (this.currentMode === 'select') {
|
|
109
|
-
this.startShapeDragging(event)
|
|
110
|
-
svg.addEventListener('mousemove', this.onShapeMouseMove)
|
|
111
|
-
svg.addEventListener('mouseup', this.onShapeMouseUp)
|
|
109
|
+
this.startPanning(event)
|
|
112
110
|
} else if (this.currentMode === 'draw') {
|
|
113
111
|
this.startDrawing(event)
|
|
114
|
-
svg.addEventListener('mousemove', this.onMouseMove)
|
|
115
|
-
svg.addEventListener('mouseup', this.onMouseUp)
|
|
116
112
|
}
|
|
117
113
|
}
|
|
118
114
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
this.
|
|
123
|
-
|
|
115
|
+
protected onSvgTouchStart = (event: TouchEvent) => {
|
|
116
|
+
if (event.touches.length === 1) {
|
|
117
|
+
this.selectedShape = null
|
|
118
|
+
this.shapes = this.shapes.map(s => {
|
|
119
|
+
return { ...s, selected: false }
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
if (this.currentMode === 'move') {
|
|
123
|
+
this.startPanning(event)
|
|
124
|
+
} else if (this.currentMode === 'draw') {
|
|
125
|
+
this.startDrawing(event)
|
|
126
|
+
}
|
|
127
|
+
} else if (event.touches.length === 2) {
|
|
128
|
+
this.startPinchZooming(event)
|
|
124
129
|
}
|
|
125
130
|
}
|
|
126
131
|
|
|
127
|
-
private startDrawing(event: MouseEvent) {
|
|
132
|
+
private startDrawing(event: MouseEvent | TouchEvent) {
|
|
128
133
|
const { x, y } = this.getSvgPoint(event)
|
|
129
134
|
|
|
130
|
-
// currentShapeType이 'text'일 경우에만 input을 보여 텍스트 입력을 받음
|
|
131
135
|
if (this.currentShapeType === 'text') {
|
|
132
136
|
const textElement = document.createElementNS('http://www.w3.org/2000/svg', 'text')
|
|
133
137
|
|
|
134
138
|
textElement.setAttribute('x', x.toString())
|
|
135
139
|
textElement.setAttribute('y', y.toString())
|
|
136
|
-
textElement.textContent = 'ABC'
|
|
140
|
+
textElement.textContent = 'ABC'
|
|
137
141
|
|
|
138
142
|
this.svg.appendChild(textElement)
|
|
139
143
|
|
|
140
|
-
// input 요소 생성
|
|
141
144
|
requestAnimationFrame(() => {
|
|
142
145
|
this.showTextInput(textElement, null, { x, y })
|
|
143
146
|
})
|
|
144
147
|
} else {
|
|
145
|
-
// 'text'가 아닌 경우 기존 도형 그리기 동작 유지
|
|
146
148
|
this.startX = x
|
|
147
149
|
this.startY = y
|
|
148
|
-
|
|
149
150
|
this.drawing = true
|
|
150
151
|
}
|
|
152
|
+
|
|
153
|
+
if ('touches' in event) {
|
|
154
|
+
this.svg.addEventListener('touchmove', this.onDrawingMouseMove)
|
|
155
|
+
this.svg.addEventListener('touchend', this.onDrawingMouseUp)
|
|
156
|
+
} else {
|
|
157
|
+
this.svg.addEventListener('mousemove', this.onDrawingMouseMove)
|
|
158
|
+
this.svg.addEventListener('mouseup', this.onDrawingMouseUp)
|
|
159
|
+
}
|
|
151
160
|
}
|
|
152
161
|
|
|
153
162
|
private setMode(mode: ImageMarkerMode, shapeType: ShapeType) {
|
|
@@ -155,18 +164,17 @@ export class OxImageMarker extends BaseImageMarker {
|
|
|
155
164
|
this.currentShapeType = shapeType
|
|
156
165
|
}
|
|
157
166
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
167
|
+
private onDrawingMouseMove = (event: MouseEvent | TouchEvent) => {
|
|
168
|
+
if (!this.drawing) {
|
|
169
|
+
return
|
|
170
|
+
}
|
|
161
171
|
|
|
162
172
|
const { x, y } = this.getSvgPoint(event)
|
|
163
173
|
|
|
164
|
-
// 최소 거리 확인 (예: 5px 이상이어야 도형을 생성)
|
|
165
174
|
const minDistance = 5
|
|
166
175
|
const distance = Math.sqrt(Math.pow(x - this.startX, 2) + Math.pow(y - this.startY, 2))
|
|
167
176
|
|
|
168
177
|
if (!this.selectedShape && distance >= minDistance) {
|
|
169
|
-
// 거리가 충분할 때만 새로운 도형을 생성
|
|
170
178
|
let newShape: Shape
|
|
171
179
|
|
|
172
180
|
switch (this.currentShapeType) {
|
|
@@ -225,14 +233,12 @@ export class OxImageMarker extends BaseImageMarker {
|
|
|
225
233
|
}
|
|
226
234
|
|
|
227
235
|
this.selectedShape = newShape
|
|
228
|
-
this.shapes = [...this.shapes, newShape]
|
|
236
|
+
this.shapes = [...this.shapes, newShape]
|
|
229
237
|
}
|
|
230
238
|
|
|
231
239
|
if (this.selectedShape) {
|
|
232
|
-
// 선택된 도형의 크기를 업데이트
|
|
233
240
|
switch (this.selectedShape.type) {
|
|
234
241
|
case 'rectangle':
|
|
235
|
-
// x, y를 업데이트하여 음의 방향으로 드래그할 때도 사각형이 올바르게 그려지도록 함
|
|
236
242
|
this.selectedShape.x = Math.min(x, this.startX)
|
|
237
243
|
this.selectedShape.y = Math.min(y, this.startY)
|
|
238
244
|
this.selectedShape.width = Math.abs(x - this.startX)
|
|
@@ -257,15 +263,22 @@ export class OxImageMarker extends BaseImageMarker {
|
|
|
257
263
|
break
|
|
258
264
|
}
|
|
259
265
|
|
|
260
|
-
this.requestUpdate()
|
|
266
|
+
this.requestUpdate()
|
|
261
267
|
}
|
|
262
268
|
}
|
|
263
269
|
|
|
264
|
-
private
|
|
270
|
+
private onDrawingMouseUp = (event: MouseEvent | TouchEvent) => {
|
|
265
271
|
this.drawing = false
|
|
272
|
+
|
|
266
273
|
const svg = this.svg as SVGSVGElement
|
|
267
|
-
|
|
268
|
-
|
|
274
|
+
|
|
275
|
+
if ('touches' in event) {
|
|
276
|
+
svg.removeEventListener('touchmove', this.onDrawingMouseMove)
|
|
277
|
+
svg.removeEventListener('touchend', this.onDrawingMouseUp)
|
|
278
|
+
} else {
|
|
279
|
+
svg.removeEventListener('mousemove', this.onDrawingMouseMove)
|
|
280
|
+
svg.removeEventListener('mouseup', this.onDrawingMouseUp)
|
|
281
|
+
}
|
|
269
282
|
|
|
270
283
|
if (this.selectedShape) {
|
|
271
284
|
this.selectedShape.selected = true
|
|
@@ -274,17 +287,18 @@ export class OxImageMarker extends BaseImageMarker {
|
|
|
274
287
|
this.requestUpdate()
|
|
275
288
|
}
|
|
276
289
|
|
|
277
|
-
|
|
278
|
-
|
|
290
|
+
private getSvgPoint(event: MouseEvent | TouchEvent) {
|
|
291
|
+
const { x, y } = this.getPointFromEvent(event)
|
|
292
|
+
|
|
279
293
|
const svg = this.svg
|
|
280
294
|
const point = svg.createSVGPoint()
|
|
281
|
-
point.x =
|
|
282
|
-
point.y =
|
|
295
|
+
point.x = x
|
|
296
|
+
point.y = y
|
|
283
297
|
const ctm = svg.getScreenCTM()!.inverse()
|
|
298
|
+
|
|
284
299
|
return point.matrixTransform(ctm)
|
|
285
300
|
}
|
|
286
301
|
|
|
287
|
-
// 도형 렌더링 함수
|
|
288
302
|
protected renderShape(shape: Shape) {
|
|
289
303
|
const commonAttrs = {
|
|
290
304
|
stroke: shape.selected ? 'blue' : 'red',
|
|
@@ -302,6 +316,7 @@ export class OxImageMarker extends BaseImageMarker {
|
|
|
302
316
|
fill="${commonAttrs.stroke}"
|
|
303
317
|
style="cursor: ${commonAttrs.cursor}; font-size: 16px;"
|
|
304
318
|
@mousedown="${(e: MouseEvent) => this.onShapeMouseDown(e, shape)}"
|
|
319
|
+
@touchstart="${(e: TouchEvent) => this.onShapeMouseDown(e, shape)}"
|
|
305
320
|
@dblclick="${(e: MouseEvent) => this.onTextDoubleClick(e, shape)}"
|
|
306
321
|
>${shape.text}</text>
|
|
307
322
|
`
|
|
@@ -318,6 +333,7 @@ export class OxImageMarker extends BaseImageMarker {
|
|
|
318
333
|
fill="${commonAttrs.fill}"
|
|
319
334
|
style="cursor: ${commonAttrs.cursor}"
|
|
320
335
|
@mousedown="${(e: MouseEvent) => this.onShapeMouseDown(e, shape)}"
|
|
336
|
+
@touchstart="${(e: TouchEvent) => this.onShapeMouseDown(e, shape)}"
|
|
321
337
|
></rect>
|
|
322
338
|
${shape.selected ? this.renderResizeHandles(shape) : ''}
|
|
323
339
|
`
|
|
@@ -333,6 +349,7 @@ export class OxImageMarker extends BaseImageMarker {
|
|
|
333
349
|
fill="${commonAttrs.fill}"
|
|
334
350
|
style="cursor: ${commonAttrs.cursor}"
|
|
335
351
|
@mousedown="${(e: MouseEvent) => this.onShapeMouseDown(e, shape)}"
|
|
352
|
+
@touchstart="${(e: TouchEvent) => this.onShapeMouseDown(e, shape)}"
|
|
336
353
|
></circle>
|
|
337
354
|
${shape.selected ? this.renderResizeHandles(shape) : ''}
|
|
338
355
|
`
|
|
@@ -346,6 +363,7 @@ export class OxImageMarker extends BaseImageMarker {
|
|
|
346
363
|
fill="${commonAttrs.fill}"
|
|
347
364
|
style="cursor: ${commonAttrs.cursor}"
|
|
348
365
|
@mousedown="${(e: MouseEvent) => this.onShapeMouseDown(e, shape)}"
|
|
366
|
+
@touchstart="${(e: TouchEvent) => this.onShapeMouseDown(e, shape)}"
|
|
349
367
|
></polygon>
|
|
350
368
|
${shape.selected ? this.renderResizeHandles(shape) : ''}
|
|
351
369
|
`
|
|
@@ -361,6 +379,7 @@ export class OxImageMarker extends BaseImageMarker {
|
|
|
361
379
|
stroke-width="${commonAttrs['stroke-width']}"
|
|
362
380
|
style="cursor: ${commonAttrs.cursor}"
|
|
363
381
|
@mousedown="${(e: MouseEvent) => this.onShapeMouseDown(e, shape)}"
|
|
382
|
+
@touchstart="${(e: TouchEvent) => this.onShapeMouseDown(e, shape)}"
|
|
364
383
|
></line>
|
|
365
384
|
${shape.selected ? this.renderResizeHandles(shape) : ''}
|
|
366
385
|
`
|
|
@@ -382,6 +401,7 @@ export class OxImageMarker extends BaseImageMarker {
|
|
|
382
401
|
marker-end="url(#arrowhead-${shape.id})"
|
|
383
402
|
style="cursor: ${commonAttrs.cursor}"
|
|
384
403
|
@mousedown="${(e: MouseEvent) => this.onShapeMouseDown(e, shape)}"
|
|
404
|
+
@touchstart="${(e: TouchEvent) => this.onShapeMouseDown(e, shape)}"
|
|
385
405
|
></line>
|
|
386
406
|
${shape.selected ? this.renderResizeHandles(shape) : ''}
|
|
387
407
|
`
|
|
@@ -391,7 +411,7 @@ export class OxImageMarker extends BaseImageMarker {
|
|
|
391
411
|
}
|
|
392
412
|
}
|
|
393
413
|
|
|
394
|
-
private onShapeMouseDown(event: MouseEvent, shape: Shape) {
|
|
414
|
+
private onShapeMouseDown(event: MouseEvent | TouchEvent, shape: Shape) {
|
|
395
415
|
event.stopPropagation()
|
|
396
416
|
|
|
397
417
|
this.shapes = this.shapes.map(s => ({
|
|
@@ -401,7 +421,6 @@ export class OxImageMarker extends BaseImageMarker {
|
|
|
401
421
|
|
|
402
422
|
this.selectedShape = this.shapes.find(s => s.id == shape.id)!
|
|
403
423
|
|
|
404
|
-
// 마우스 클릭 좌표와 도형의 현재 좌표 차이 계산
|
|
405
424
|
const { x, y } = this.getSvgPoint(event)
|
|
406
425
|
|
|
407
426
|
switch (shape.type) {
|
|
@@ -428,20 +447,24 @@ export class OxImageMarker extends BaseImageMarker {
|
|
|
428
447
|
break
|
|
429
448
|
}
|
|
430
449
|
|
|
431
|
-
// 드래그 이벤트 리스너 추가
|
|
432
450
|
const svg = this.svg as SVGSVGElement
|
|
433
|
-
|
|
434
|
-
|
|
451
|
+
|
|
452
|
+
if ('touches' in event) {
|
|
453
|
+
svg.addEventListener('touchmove', this.onShapeMouseMove)
|
|
454
|
+
svg.addEventListener('touchend', this.onShapeMouseUp)
|
|
455
|
+
} else {
|
|
456
|
+
svg.addEventListener('mousemove', this.onShapeMouseMove)
|
|
457
|
+
svg.addEventListener('mouseup', this.onShapeMouseUp)
|
|
458
|
+
}
|
|
435
459
|
}
|
|
436
460
|
|
|
437
|
-
private onShapeMouseMove = (event: MouseEvent) => {
|
|
461
|
+
private onShapeMouseMove = (event: MouseEvent | TouchEvent) => {
|
|
438
462
|
if (!this.selectedShape) {
|
|
439
463
|
return
|
|
440
464
|
}
|
|
441
465
|
|
|
442
466
|
const { x, y } = this.getSvgPoint(event)
|
|
443
467
|
|
|
444
|
-
// 마우스 이동에 따라 도형의 위치 업데이트
|
|
445
468
|
switch (this.selectedShape.type) {
|
|
446
469
|
case 'text':
|
|
447
470
|
case 'rectangle':
|
|
@@ -473,26 +496,19 @@ export class OxImageMarker extends BaseImageMarker {
|
|
|
473
496
|
break
|
|
474
497
|
}
|
|
475
498
|
|
|
476
|
-
this.requestUpdate()
|
|
499
|
+
this.requestUpdate()
|
|
477
500
|
}
|
|
478
501
|
|
|
479
|
-
private onShapeMouseUp = () => {
|
|
502
|
+
private onShapeMouseUp = (event: MouseEvent | TouchEvent) => {
|
|
480
503
|
const svg = this.svg as SVGSVGElement
|
|
481
|
-
svg.removeEventListener('mousemove', this.onShapeMouseMove)
|
|
482
|
-
svg.removeEventListener('mouseup', this.onShapeMouseUp)
|
|
483
|
-
}
|
|
484
504
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
505
|
+
if ('touches' in event) {
|
|
506
|
+
svg.removeEventListener('touchmove', this.onShapeMouseMove)
|
|
507
|
+
svg.removeEventListener('touchend', this.onShapeMouseUp)
|
|
508
|
+
} else {
|
|
509
|
+
svg.removeEventListener('mousemove', this.onShapeMouseMove)
|
|
510
|
+
svg.removeEventListener('mouseup', this.onShapeMouseUp)
|
|
488
511
|
}
|
|
489
|
-
|
|
490
|
-
const [vbWidth, vbHeight] = this.viewBox.split(' ').map(Number)
|
|
491
|
-
|
|
492
|
-
const scaleX = vbWidth / this.svg.clientWidth
|
|
493
|
-
const scaleY = vbHeight / this.svg.clientHeight
|
|
494
|
-
|
|
495
|
-
return Math.max(scaleX, scaleY)
|
|
496
512
|
}
|
|
497
513
|
|
|
498
514
|
private renderResizeHandles(shape: Shape) {
|
|
@@ -546,6 +562,7 @@ export class OxImageMarker extends BaseImageMarker {
|
|
|
546
562
|
class="resize-handle"
|
|
547
563
|
style="cursor: ${handle.cursor};"
|
|
548
564
|
@mousedown="${(e: MouseEvent) => this.onHandleMouseDown(e, shape, handle)}"
|
|
565
|
+
@touchstart="${(e: TouchEvent) => this.onHandleMouseDown(e, shape, handle)}"
|
|
549
566
|
></rect>
|
|
550
567
|
</g>
|
|
551
568
|
`
|
|
@@ -553,27 +570,73 @@ export class OxImageMarker extends BaseImageMarker {
|
|
|
553
570
|
`
|
|
554
571
|
}
|
|
555
572
|
|
|
556
|
-
private onHandleMouseDown(event: MouseEvent, shape: Shape, handle: any) {
|
|
573
|
+
private onHandleMouseDown(event: MouseEvent | TouchEvent, shape: Shape, handle: any) {
|
|
557
574
|
event.stopPropagation()
|
|
575
|
+
|
|
558
576
|
this.selectedShape = shape
|
|
559
577
|
this.offsetX = handle.x
|
|
560
578
|
this.offsetY = handle.y
|
|
561
579
|
const svg = this.svg as SVGSVGElement
|
|
562
|
-
|
|
563
|
-
|
|
580
|
+
|
|
581
|
+
if ('touches' in event) {
|
|
582
|
+
svg.addEventListener('touchmove', this.onHandleMouseMove)
|
|
583
|
+
svg.addEventListener('touchend', this.onHandleMouseUp)
|
|
584
|
+
} else {
|
|
585
|
+
svg.addEventListener('mousemove', this.onHandleMouseMove)
|
|
586
|
+
svg.addEventListener('mouseup', this.onHandleMouseUp)
|
|
587
|
+
}
|
|
564
588
|
}
|
|
565
589
|
|
|
566
|
-
private onHandleMouseMove = (event: MouseEvent) => {
|
|
567
|
-
if (!this.selectedShape)
|
|
590
|
+
private onHandleMouseMove = (event: MouseEvent | TouchEvent) => {
|
|
591
|
+
if (!this.selectedShape) {
|
|
592
|
+
return
|
|
593
|
+
}
|
|
594
|
+
|
|
568
595
|
const { x, y } = this.getSvgPoint(event)
|
|
569
596
|
|
|
570
597
|
switch (this.selectedShape.type) {
|
|
571
|
-
case 'text':
|
|
598
|
+
// case 'text':
|
|
599
|
+
// this.selectedShape.width = Math.abs(x - this.selectedShape.x!)
|
|
600
|
+
// this.selectedShape.height = Math.abs(y - this.selectedShape.y!)
|
|
601
|
+
// if (x < this.selectedShape.x!) this.selectedShape.x = x
|
|
602
|
+
// if (y < this.selectedShape.y!) this.selectedShape.y = y
|
|
603
|
+
// break
|
|
572
604
|
case 'rectangle':
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
605
|
+
const startX = this.selectedShape.x!
|
|
606
|
+
const startY = this.selectedShape.y!
|
|
607
|
+
const width = this.selectedShape.width!
|
|
608
|
+
const height = this.selectedShape.height!
|
|
609
|
+
|
|
610
|
+
// 핸들의 좌표에 따라 너비 및 높이 조정
|
|
611
|
+
if (this.offsetX === startX && this.offsetY === startY) {
|
|
612
|
+
// 상좌 핸들 (왼쪽 위)
|
|
613
|
+
this.selectedShape.x = x
|
|
614
|
+
this.selectedShape.y = y
|
|
615
|
+
this.selectedShape.width = width + (startX - x)
|
|
616
|
+
this.selectedShape.height = height + (startY - y)
|
|
617
|
+
} else if (this.offsetX === startX + width && this.offsetY === startY) {
|
|
618
|
+
// 상우 핸들 (오른쪽 위)
|
|
619
|
+
this.selectedShape.y = y
|
|
620
|
+
this.selectedShape.width = Math.abs(x - startX)
|
|
621
|
+
this.selectedShape.height = height + (startY - y)
|
|
622
|
+
} else if (this.offsetX === startX && this.offsetY === startY + height) {
|
|
623
|
+
// 하좌 핸들 (왼쪽 아래)
|
|
624
|
+
this.selectedShape.x = x
|
|
625
|
+
this.selectedShape.width = width + (startX - x)
|
|
626
|
+
this.selectedShape.height = Math.abs(y - startY)
|
|
627
|
+
} else if (this.offsetX === startX + width && this.offsetY === startY + height) {
|
|
628
|
+
// 하우 핸들 (오른쪽 아래)
|
|
629
|
+
this.selectedShape.width = Math.abs(x - startX)
|
|
630
|
+
this.selectedShape.height = Math.abs(y - startY)
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// 최소 너비 및 높이 유지
|
|
634
|
+
this.selectedShape.width = Math.max(10, this.selectedShape.width!)
|
|
635
|
+
this.selectedShape.height = Math.max(10, this.selectedShape.height!)
|
|
636
|
+
|
|
637
|
+
this.offsetX = x
|
|
638
|
+
this.offsetY = y
|
|
639
|
+
|
|
577
640
|
break
|
|
578
641
|
case 'circle':
|
|
579
642
|
const dx = x - this.selectedShape.cx!
|
|
@@ -607,10 +670,17 @@ export class OxImageMarker extends BaseImageMarker {
|
|
|
607
670
|
this.requestUpdate()
|
|
608
671
|
}
|
|
609
672
|
|
|
610
|
-
private onHandleMouseUp = () => {
|
|
673
|
+
private onHandleMouseUp = (event: MouseEvent | TouchEvent) => {
|
|
611
674
|
const svg = this.svg as SVGSVGElement
|
|
612
|
-
|
|
613
|
-
|
|
675
|
+
|
|
676
|
+
if ('touches' in event) {
|
|
677
|
+
svg.removeEventListener('touchmove', this.onHandleMouseMove)
|
|
678
|
+
svg.removeEventListener('touchend', this.onHandleMouseUp)
|
|
679
|
+
} else {
|
|
680
|
+
svg.removeEventListener('mousemove', this.onHandleMouseMove)
|
|
681
|
+
svg.removeEventListener('mouseup', this.onHandleMouseUp)
|
|
682
|
+
}
|
|
683
|
+
|
|
614
684
|
this.selectedShape = null
|
|
615
685
|
}
|
|
616
686
|
|
|
@@ -623,23 +693,18 @@ export class OxImageMarker extends BaseImageMarker {
|
|
|
623
693
|
}
|
|
624
694
|
|
|
625
695
|
private showTextInput(textElement: SVGTextElement, shape: Shape | null = null, options?: { x: number; y: number }) {
|
|
626
|
-
// input 요소 생성
|
|
627
696
|
const input: HTMLInputElement = document.createElement('input')
|
|
628
697
|
input.style.position = 'absolute'
|
|
629
698
|
|
|
630
|
-
// SVG 텍스트의 CTM 얻기
|
|
631
699
|
const ctm = textElement.getScreenCTM() || { a: 1, e: 0, d: 1, f: 0 }
|
|
632
700
|
const bbox = textElement.getBBox()
|
|
633
701
|
|
|
634
|
-
// 텍스트의 화면 위치 계산
|
|
635
702
|
const screenX = bbox.x * ctm.a + ctm.e
|
|
636
703
|
const screenY = bbox.y * ctm.d + ctm.f
|
|
637
704
|
|
|
638
|
-
// input 위치 조정 (scale과 transform을 고려한 정확한 좌표로 변환)
|
|
639
705
|
input.style.left = `${screenX}px`
|
|
640
706
|
input.style.top = `${screenY}px`
|
|
641
707
|
|
|
642
|
-
// 텍스트 폰트 스타일을 가져와 input에 적용
|
|
643
708
|
const computedStyle = window.getComputedStyle(textElement)
|
|
644
709
|
const fontSize = parseFloat(computedStyle.fontSize) * ctm.a
|
|
645
710
|
const fontFamily = computedStyle.fontFamily
|
|
@@ -647,7 +712,6 @@ export class OxImageMarker extends BaseImageMarker {
|
|
|
647
712
|
const textColor = computedStyle.fill || computedStyle.color
|
|
648
713
|
const lineHeight = parseFloat(computedStyle.lineHeight) * ctm.d
|
|
649
714
|
|
|
650
|
-
// input 스타일을 텍스트와 일치시킴
|
|
651
715
|
input.style.fontSize = `${fontSize}px`
|
|
652
716
|
input.style.fontFamily = fontFamily
|
|
653
717
|
input.style.fontWeight = fontWeight
|
|
@@ -663,15 +727,12 @@ export class OxImageMarker extends BaseImageMarker {
|
|
|
663
727
|
input.value = shape.text || ''
|
|
664
728
|
}
|
|
665
729
|
|
|
666
|
-
// SVG 텍스트 숨김
|
|
667
730
|
textElement.style.visibility = 'hidden'
|
|
668
731
|
|
|
669
|
-
// input 요소를 추가하고, 텍스트 길이에 따라 너비 조정
|
|
670
732
|
document.body.appendChild(input)
|
|
671
|
-
this.adjustInputWidth(input)
|
|
733
|
+
this.adjustInputWidth(input)
|
|
672
734
|
requestAnimationFrame(() => input.focus())
|
|
673
735
|
|
|
674
|
-
// 입력 중에 동적으로 input의 너비 조정
|
|
675
736
|
input.addEventListener('input', () => {
|
|
676
737
|
this.adjustInputWidth(input)
|
|
677
738
|
})
|
|
@@ -733,14 +794,12 @@ export class OxImageMarker extends BaseImageMarker {
|
|
|
733
794
|
})
|
|
734
795
|
}
|
|
735
796
|
|
|
736
|
-
// 텍스트 길이에 맞춰 input 박스의 너비를 조정하는 함수 (최소 길이 유지)
|
|
737
797
|
private adjustInputWidth(input: HTMLInputElement) {
|
|
738
|
-
const minWidth = 50
|
|
798
|
+
const minWidth = 50
|
|
739
799
|
const textWidth = this.measureTextWidth(input.value || input.placeholder, input)
|
|
740
|
-
input.style.width = `${Math.max(textWidth + 10, minWidth)}px`
|
|
800
|
+
input.style.width = `${Math.max(textWidth + 10, minWidth)}px`
|
|
741
801
|
}
|
|
742
802
|
|
|
743
|
-
// 텍스트 길이를 측정하는 함수
|
|
744
803
|
private measureTextWidth(text: string, input: HTMLInputElement): number {
|
|
745
804
|
const canvas = document.createElement('canvas')
|
|
746
805
|
const context = canvas.getContext('2d')!
|
|
@@ -748,4 +807,26 @@ export class OxImageMarker extends BaseImageMarker {
|
|
|
748
807
|
|
|
749
808
|
return context.measureText(text).width
|
|
750
809
|
}
|
|
810
|
+
|
|
811
|
+
private onFocus = () => {
|
|
812
|
+
this.addEventListener('keydown', this.handleKeyDown)
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
private onBlur = () => {
|
|
816
|
+
this.removeEventListener('keydown', this.handleKeyDown)
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
private handleKeyDown = (event: KeyboardEvent) => {
|
|
820
|
+
if ((event.key === 'Delete' || event.key === 'Backspace') && this.selectedShape) {
|
|
821
|
+
this.deleteSelectedShape()
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
private deleteSelectedShape() {
|
|
826
|
+
if (this.selectedShape) {
|
|
827
|
+
this.shapes = this.shapes.filter(s => s.id !== this.selectedShape!.id)
|
|
828
|
+
this.selectedShape = null
|
|
829
|
+
this.requestUpdate()
|
|
830
|
+
}
|
|
831
|
+
}
|
|
751
832
|
}
|
|
@@ -55,6 +55,22 @@ const Template: Story<ArgTypes> = ({ imageUrl, shapes = [] }: ArgTypes) => html`
|
|
|
55
55
|
</style>
|
|
56
56
|
|
|
57
57
|
<style>
|
|
58
|
+
html,
|
|
59
|
+
body {
|
|
60
|
+
margin: 0;
|
|
61
|
+
padding: 0;
|
|
62
|
+
overflow: hidden;
|
|
63
|
+
|
|
64
|
+
overscroll-behavior-y: none;
|
|
65
|
+
|
|
66
|
+
/* This is a font-stack that tries to use the system-default sans-serifs first */
|
|
67
|
+
font-family: Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
|
|
68
|
+
line-height: 1.5;
|
|
69
|
+
-webkit-font-smoothing: antialiased;
|
|
70
|
+
|
|
71
|
+
accent-color: var(--md-sys-color-primary);
|
|
72
|
+
}
|
|
73
|
+
|
|
58
74
|
body {
|
|
59
75
|
width: 100%;
|
|
60
76
|
height: 800px;
|