@operato/image-marker 8.0.0-alpha.33 → 8.0.0-alpha.38
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 +19 -0
- package/dist/src/base-image-marker.d.ts +21 -26
- package/dist/src/base-image-marker.js +119 -138
- package/dist/src/base-image-marker.js.map +1 -1
- package/dist/src/default-image-provider.d.ts +4 -0
- package/dist/src/default-image-provider.js +6 -0
- package/dist/src/default-image-provider.js.map +1 -0
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.js +5 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/ox-image-marker-view.js +4 -4
- package/dist/src/ox-image-marker-view.js.map +1 -1
- package/dist/src/ox-image-marker.d.ts +11 -11
- package/dist/src/ox-image-marker.js +173 -226
- package/dist/src/ox-image-marker.js.map +1 -1
- package/dist/src/types.d.ts +13 -1
- package/dist/src/types.js.map +1 -1
- package/dist/stories/test-image-provider.d.ts +7 -0
- package/dist/stories/test-image-provider.js +42 -0
- package/dist/stories/test-image-provider.js.map +1 -0
- package/dist/stories/test-image-provider.stories.d.ts +33 -0
- package/dist/stories/test-image-provider.stories.js +119 -0
- package/dist/stories/test-image-provider.stories.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +15 -14
- package/src/base-image-marker.ts +136 -159
- package/src/default-image-provider.ts +7 -0
- package/src/index.ts +6 -0
- package/src/ox-image-marker-view.ts +4 -4
- package/src/ox-image-marker.ts +187 -235
- package/src/types.ts +15 -1
- package/stories/test-image-provider.stories.ts +135 -0
- package/stories/test-image-provider.ts +51 -0
package/src/base-image-marker.ts
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
|
-
import { LitElement, css, html, svg, nothing } from 'lit'
|
|
1
|
+
import { LitElement, css, html, svg, nothing, PropertyValues } from 'lit'
|
|
2
2
|
import { property, state, query } from 'lit/decorators.js'
|
|
3
|
+
import { debounce } from 'lodash-es'
|
|
3
4
|
|
|
4
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
GestureEventDoubleTap,
|
|
7
|
+
GestureEventDrag,
|
|
8
|
+
GestureEventPinch,
|
|
9
|
+
GestureHelper
|
|
10
|
+
} from '@operato/utils/gesture-helper.js'
|
|
11
|
+
import { ImageProvider, Shape } from './types'
|
|
12
|
+
import { DefaultImageProvider } from './default-image-provider'
|
|
5
13
|
|
|
6
14
|
export class BaseImageMarker extends LitElement {
|
|
7
15
|
static styles = [
|
|
@@ -22,6 +30,11 @@ export class BaseImageMarker extends LitElement {
|
|
|
22
30
|
cursor: pointer;
|
|
23
31
|
width: 100%;
|
|
24
32
|
height: 100%;
|
|
33
|
+
touch-action: none;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.link:hover {
|
|
37
|
+
cursor: pointer;
|
|
25
38
|
}
|
|
26
39
|
`
|
|
27
40
|
]
|
|
@@ -29,217 +42,174 @@ export class BaseImageMarker extends LitElement {
|
|
|
29
42
|
@property({ type: String }) imageUrl: string = ''
|
|
30
43
|
@property({ type: Array }) shapes: Shape[] = []
|
|
31
44
|
|
|
45
|
+
@state() protected currentImage?: string
|
|
32
46
|
@state() protected viewBox = '0 0 800 600'
|
|
33
47
|
@state() protected zoomLevel = 1
|
|
48
|
+
@state() private imageProvider?: ImageProvider = new DefaultImageProvider()
|
|
34
49
|
|
|
50
|
+
protected debouncedUpdateImage = debounce(this.updateImage.bind(this), 300)
|
|
35
51
|
protected zoomFactor = 0.1
|
|
36
52
|
protected zoomThreshold = 0.05
|
|
37
|
-
protected isPanning = false
|
|
38
|
-
protected panStart = { x: 0, y: 0 }
|
|
39
|
-
protected panInitiated = false
|
|
40
|
-
protected panThreshold = 20
|
|
41
53
|
|
|
42
|
-
protected initialViewBox
|
|
43
|
-
private initialPinchDistance: number | null = null
|
|
44
|
-
private initialZoomLevel: number = 1
|
|
45
|
-
private lastTouchEnd: number = 0
|
|
54
|
+
protected initialViewBox: string | null = null
|
|
46
55
|
|
|
47
56
|
@query('svg') protected svg!: SVGSVGElement
|
|
57
|
+
@query('image') protected image!: SVGImageElement
|
|
48
58
|
|
|
49
|
-
|
|
50
|
-
const img = new Image()
|
|
51
|
-
img.onload = () => {
|
|
52
|
-
const width = img.naturalWidth
|
|
53
|
-
const height = img.naturalHeight
|
|
54
|
-
this.viewBox = `0 0 ${width} ${height}`
|
|
55
|
-
this.initialViewBox = `0 0 ${width} ${height}`
|
|
56
|
-
}
|
|
57
|
-
img.src = this.imageUrl
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
protected onSvgWheel(event: WheelEvent) {
|
|
61
|
-
event.preventDefault()
|
|
59
|
+
private gestureHelper: GestureHelper | null = null
|
|
62
60
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
this.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const mouseX = vbX + (offsetX / this.svg.clientWidth) * vbWidth
|
|
70
|
-
const mouseY = vbY + (offsetY / this.svg.clientHeight) * vbHeight
|
|
61
|
+
connectedCallback(): void {
|
|
62
|
+
super.connectedCallback()
|
|
63
|
+
requestAnimationFrame(() => {
|
|
64
|
+
this.gestureHelper = new GestureHelper(this.svg)
|
|
65
|
+
})
|
|
66
|
+
}
|
|
71
67
|
|
|
72
|
-
|
|
73
|
-
|
|
68
|
+
disconnectedCallback(): void {
|
|
69
|
+
super.disconnectedCallback()
|
|
74
70
|
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
this.gestureHelper?.dispose()
|
|
72
|
+
this.gestureHelper = null
|
|
73
|
+
}
|
|
77
74
|
|
|
78
|
-
|
|
75
|
+
protected updated(changes: PropertyValues): void {
|
|
76
|
+
if (changes.has('imageUrl') || changes.has('imageProvider')) {
|
|
77
|
+
this.initialViewBox = null
|
|
78
|
+
this.updateImage()
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
this.startPanning(event)
|
|
86
|
-
|
|
87
|
-
this.svg.addEventListener('mousemove', this.onPanMouseMove)
|
|
88
|
-
this.svg.addEventListener('mouseup', this.onPanMouseUp)
|
|
82
|
+
public setImageProvider(provider: ImageProvider) {
|
|
83
|
+
this.imageProvider = provider
|
|
89
84
|
}
|
|
90
85
|
|
|
91
|
-
protected
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
const result = tapLength < 300 && event.touches.length == 1
|
|
96
|
-
this.lastTouchEnd = currentTime
|
|
86
|
+
protected onImageLoad(event: Event) {
|
|
87
|
+
const element = event.target as SVGImageElement
|
|
88
|
+
const img = new Image()
|
|
97
89
|
|
|
98
|
-
|
|
99
|
-
|
|
90
|
+
img.onload = () => {
|
|
91
|
+
const width = img.naturalWidth
|
|
92
|
+
const height = img.naturalHeight
|
|
100
93
|
|
|
101
|
-
|
|
102
|
-
|
|
94
|
+
if (!this.initialViewBox) {
|
|
95
|
+
this.initialViewBox = `0 0 ${width} ${height}`
|
|
96
|
+
this.viewBox = this.initialViewBox
|
|
103
97
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
98
|
+
element.setAttribute('width', width.toString())
|
|
99
|
+
element.setAttribute('height', height.toString())
|
|
100
|
+
element.setAttribute('preserveAspectRatio', 'xMidYMid meet')
|
|
101
|
+
} else {
|
|
102
|
+
const [initialX, initialY, initialWidth, initialHeight] = this.initialViewBox.split(' ').map(Number)
|
|
103
|
+
const scale = initialWidth / width
|
|
107
104
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
this.startPinchZooming(event)
|
|
105
|
+
element.setAttribute('width', (width * scale).toString())
|
|
106
|
+
element.setAttribute('height', (height * scale).toString())
|
|
107
|
+
}
|
|
112
108
|
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
protected startPinchZooming(event: TouchEvent) {
|
|
116
|
-
const touch1 = event.touches[0]
|
|
117
|
-
const touch2 = event.touches[1]
|
|
118
|
-
|
|
119
|
-
const dx = touch2.clientX - touch1.clientX
|
|
120
|
-
const dy = touch2.clientY - touch1.clientY
|
|
121
109
|
|
|
122
|
-
|
|
123
|
-
this.initialZoomLevel = this.zoomLevel
|
|
124
|
-
|
|
125
|
-
this.svg.addEventListener('touchmove', this.onZoomTouchMove)
|
|
126
|
-
this.svg.addEventListener('touchend', this.onZoomTouchEnd)
|
|
110
|
+
img.src = this.currentImage!
|
|
127
111
|
}
|
|
128
112
|
|
|
129
|
-
protected
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (event.touches.length === 2 && this.initialPinchDistance) {
|
|
133
|
-
const touch1 = event.touches[0]
|
|
134
|
-
const touch2 = event.touches[1]
|
|
135
|
-
const dx = touch2.clientX - touch1.clientX
|
|
136
|
-
const dy = touch2.clientY - touch1.clientY
|
|
137
|
-
const currentPinchDistance = Math.sqrt(dx * dx + dy * dy)
|
|
138
|
-
|
|
139
|
-
const scaleFactor = currentPinchDistance / this.initialPinchDistance
|
|
140
|
-
|
|
141
|
-
// 줌 threshold 확인 후 줌 적용
|
|
142
|
-
if (Math.abs(scaleFactor - 1) > this.zoomThreshold) {
|
|
143
|
-
this.zoomLevel = this.initialZoomLevel * scaleFactor
|
|
144
|
-
this.applyZoom()
|
|
145
|
-
}
|
|
113
|
+
protected async updateImage() {
|
|
114
|
+
if (!this.imageProvider) {
|
|
115
|
+
throw new Error('ImageProvider is not set')
|
|
146
116
|
}
|
|
147
|
-
}
|
|
148
117
|
|
|
149
|
-
|
|
150
|
-
|
|
118
|
+
if (!this.imageUrl) {
|
|
119
|
+
return
|
|
120
|
+
}
|
|
151
121
|
|
|
152
|
-
|
|
153
|
-
this.
|
|
122
|
+
const options = {
|
|
123
|
+
id: this.imageUrl, // 이미지 ID는 imageUrl 속성을 사용
|
|
124
|
+
scale: this.zoomLevel, // scale은 zoomLevel 속성을 사용
|
|
125
|
+
offsetX: 0, // offsetX는 0으로 설정
|
|
126
|
+
offsetY: 0, // offsetY는 0으로 설정
|
|
127
|
+
currentImage: this.currentImage // 기존 이미지 객체 전달
|
|
154
128
|
}
|
|
155
129
|
|
|
156
|
-
this.
|
|
157
|
-
this.svg.removeEventListener('touchend', this.onZoomTouchEnd)
|
|
130
|
+
this.currentImage = await this.imageProvider.getImage(options)
|
|
158
131
|
}
|
|
159
132
|
|
|
160
|
-
protected
|
|
161
|
-
|
|
162
|
-
const initialViewBoxNumbers = this.initialViewBox.split(' ').map(Number)
|
|
163
|
-
const newVbWidth = initialViewBoxNumbers[2] / this.zoomLevel
|
|
164
|
-
const newVbHeight = initialViewBoxNumbers[3] / this.zoomLevel
|
|
165
|
-
const newVbX = vbX + (vbWidth - newVbWidth) / 2
|
|
166
|
-
const newVbY = vbY + (vbHeight - newVbHeight) / 2
|
|
133
|
+
protected onSvgWheel(event: WheelEvent) {
|
|
134
|
+
event.preventDefault()
|
|
167
135
|
|
|
168
|
-
|
|
169
|
-
|
|
136
|
+
const { offsetX, offsetY, deltaY } = event
|
|
137
|
+
const scale = deltaY > 0 ? 1 - this.zoomFactor : 1 + this.zoomFactor
|
|
170
138
|
|
|
171
|
-
|
|
172
|
-
this.resetZoom()
|
|
139
|
+
this.zoom(scale, offsetX, offsetY)
|
|
173
140
|
}
|
|
174
141
|
|
|
175
|
-
protected
|
|
176
|
-
|
|
177
|
-
this.zoomLevel = 1
|
|
178
|
-
}
|
|
142
|
+
protected onSvgPinch(e: GestureEventPinch) {
|
|
143
|
+
e.stopPropagation()
|
|
179
144
|
|
|
180
|
-
|
|
181
|
-
const { x, y } = this.getPointFromEvent(event)
|
|
145
|
+
const { scale, centerX, centerY } = e.detail
|
|
182
146
|
|
|
183
|
-
|
|
184
|
-
this.panStart = { x, y }
|
|
185
|
-
this.panInitiated = false // 팬 동작이 아직 시작되지 않았음을 나타냄
|
|
186
|
-
this.svg.classList.add('dragging')
|
|
147
|
+
console.log('pinch', e.detail)
|
|
187
148
|
|
|
188
|
-
|
|
189
|
-
this.svg.addEventListener('touchmove', this.onPanMouseMove)
|
|
190
|
-
this.svg.addEventListener('touchend', this.onPanMouseUp)
|
|
191
|
-
} else {
|
|
192
|
-
this.svg.addEventListener('mousemove', this.onPanMouseMove)
|
|
193
|
-
this.svg.addEventListener('mouseup', this.onPanMouseUp)
|
|
194
|
-
}
|
|
149
|
+
this.zoom(scale, centerX, centerY)
|
|
195
150
|
}
|
|
196
151
|
|
|
197
|
-
protected
|
|
198
|
-
|
|
199
|
-
return
|
|
200
|
-
}
|
|
201
|
-
event.preventDefault()
|
|
202
|
-
|
|
203
|
-
const { x, y } = this.getPointFromEvent(event)
|
|
152
|
+
protected onSvgDoubleTap(e: GestureEventDoubleTap) {
|
|
153
|
+
e.stopPropagation()
|
|
204
154
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
if (!this.panInitiated && Math.sqrt(dx * dx + dy * dy) < this.panThreshold) {
|
|
209
|
-
return
|
|
210
|
-
}
|
|
155
|
+
this.reset()
|
|
156
|
+
}
|
|
211
157
|
|
|
212
|
-
|
|
158
|
+
protected onSvgDrag(e: GestureEventDrag) {
|
|
159
|
+
const { deltaX, deltaY } = e.detail
|
|
160
|
+
console.log('drag', e.detail)
|
|
213
161
|
|
|
214
162
|
const [vbX, vbY, vbWidth, vbHeight] = this.viewBox.split(' ').map(Number)
|
|
163
|
+
|
|
215
164
|
const scaleX = vbWidth / this.svg.clientWidth
|
|
216
165
|
const scaleY = vbHeight / this.svg.clientHeight
|
|
217
166
|
const scale = Math.max(scaleX, scaleY)
|
|
218
167
|
|
|
219
|
-
const newVbX = vbX -
|
|
220
|
-
const newVbY = vbY -
|
|
168
|
+
const newVbX = vbX - deltaX * scale
|
|
169
|
+
const newVbY = vbY - deltaY * scale
|
|
221
170
|
|
|
222
171
|
this.viewBox = `${newVbX} ${newVbY} ${vbWidth} ${vbHeight}`
|
|
223
|
-
|
|
172
|
+
|
|
173
|
+
this.debouncedUpdateImage()
|
|
224
174
|
}
|
|
225
175
|
|
|
226
|
-
|
|
227
|
-
|
|
176
|
+
zoom(scale: number, offsetX: number, offsetY: number) {
|
|
177
|
+
if (Math.abs(scale) > this.zoomThreshold) {
|
|
178
|
+
this.zoomLevel *= scale
|
|
179
|
+
|
|
180
|
+
const [vbX, vbY, vbWidth, vbHeight] = this.viewBox.split(' ').map(Number)
|
|
181
|
+
const mouseX = vbX + (offsetX / this.svg.clientWidth) * vbWidth
|
|
182
|
+
const mouseY = vbY + (offsetY / this.svg.clientHeight) * vbHeight
|
|
228
183
|
|
|
229
|
-
|
|
184
|
+
const newVbWidth = vbWidth / scale
|
|
185
|
+
const newVbHeight = vbHeight / scale
|
|
230
186
|
|
|
231
|
-
|
|
232
|
-
|
|
187
|
+
const newVbX = mouseX - (offsetX / this.svg.clientWidth) * newVbWidth
|
|
188
|
+
const newVbY = mouseY - (offsetY / this.svg.clientHeight) * newVbHeight
|
|
233
189
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
} else {
|
|
238
|
-
svg.removeEventListener('mousemove', this.onPanMouseMove)
|
|
239
|
-
svg.removeEventListener('mouseup', this.onPanMouseUp)
|
|
190
|
+
this.viewBox = `${newVbX} ${newVbY} ${newVbWidth} ${newVbHeight}`
|
|
191
|
+
|
|
192
|
+
this.debouncedUpdateImage()
|
|
240
193
|
}
|
|
241
194
|
}
|
|
242
195
|
|
|
196
|
+
protected reset() {
|
|
197
|
+
this.viewBox = this.initialViewBox!
|
|
198
|
+
this.zoomLevel = 1
|
|
199
|
+
|
|
200
|
+
this.debouncedUpdateImage()
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
protected handleLinkClick(shape: Shape) {
|
|
204
|
+
this.dispatchEvent(
|
|
205
|
+
new CustomEvent('link-clicked', {
|
|
206
|
+
detail: { shapeId: shape.id, link: shape.link },
|
|
207
|
+
bubbles: true,
|
|
208
|
+
composed: true
|
|
209
|
+
})
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
|
|
243
213
|
protected renderShape(shape: Shape) {
|
|
244
214
|
const commonAttrs = {
|
|
245
215
|
stroke: 'red',
|
|
@@ -264,21 +234,28 @@ export class BaseImageMarker extends LitElement {
|
|
|
264
234
|
</marker>
|
|
265
235
|
</defs>
|
|
266
236
|
<line x1="${shape.x1}" y1="${shape.y1}" x2="${shape.x2}" y2="${shape.y2}" stroke="${commonAttrs.stroke}" stroke-width="${commonAttrs['stroke-width']}" marker-end="url(#arrowhead-${shape.id})"></line>`
|
|
237
|
+
case 'link':
|
|
238
|
+
return svg`
|
|
239
|
+
<rect class="link" x="${shape.x}" y="${shape.y}" width="${shape.width}" height="${shape.height}"
|
|
240
|
+
stroke="${commonAttrs.stroke}" stroke-width="${commonAttrs['stroke-width']}"
|
|
241
|
+
fill="${commonAttrs.fill}" fill-opacity="0.5" @click="${() => this.handleLinkClick(shape)}">
|
|
242
|
+
</rect>`
|
|
267
243
|
default:
|
|
268
244
|
return nothing
|
|
269
245
|
}
|
|
270
246
|
}
|
|
271
247
|
|
|
272
|
-
protected getPointFromEvent(event: MouseEvent |
|
|
273
|
-
if ('
|
|
248
|
+
protected getPointFromEvent(event: MouseEvent | PointerEvent | GestureEventDrag) {
|
|
249
|
+
if ('clientX' in event) {
|
|
274
250
|
return {
|
|
275
|
-
x: event.
|
|
276
|
-
y: event.
|
|
251
|
+
x: event.clientX,
|
|
252
|
+
y: event.clientY
|
|
277
253
|
}
|
|
278
254
|
} else {
|
|
255
|
+
const { clientX, clientY } = event.detail
|
|
279
256
|
return {
|
|
280
|
-
x:
|
|
281
|
-
y:
|
|
257
|
+
x: clientX,
|
|
258
|
+
y: clientY
|
|
282
259
|
}
|
|
283
260
|
}
|
|
284
261
|
}
|
package/src/index.ts
CHANGED
|
@@ -10,12 +10,12 @@ export class OxImageMarkerView extends BaseImageMarker {
|
|
|
10
10
|
<div class="container">
|
|
11
11
|
<svg
|
|
12
12
|
@wheel=${this.onSvgWheel}
|
|
13
|
-
@
|
|
14
|
-
@
|
|
15
|
-
@
|
|
13
|
+
@doubletap=${this.onSvgDoubleTap}
|
|
14
|
+
@pinch=${this.onSvgPinch}
|
|
15
|
+
@drag=${this.onSvgDrag}
|
|
16
16
|
viewBox=${this.viewBox}
|
|
17
17
|
>
|
|
18
|
-
<image href=${this.
|
|
18
|
+
<image href=${this.currentImage} @load=${this.onImageLoad}></image>
|
|
19
19
|
${this.shapes.map(shape => this.renderShape(shape))}
|
|
20
20
|
</svg>
|
|
21
21
|
</div>
|