@operato/image-marker 8.0.0-alpha.29 → 8.0.0-alpha.37

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