@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.
- package/CHANGELOG.md +18 -0
- package/dist/src/base-image-marker.d.ts +19 -26
- package/dist/src/base-image-marker.js +93 -141
- 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/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 +10 -0
- 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 +41 -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 +105 -160
- package/src/default-image-provider.ts +7 -0
- package/src/ox-image-marker-view.ts +4 -4
- package/src/ox-image-marker.ts +187 -235
- package/src/types.ts +12 -0
- package/stories/test-image-provider.stories.ts +135 -0
- package/stories/test-image-provider.ts +49 -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,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
|
|
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
|
-
|
|
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
|
-
|
|
79
|
-
|
|
56
|
+
connectedCallback(): void {
|
|
57
|
+
super.connectedCallback()
|
|
58
|
+
requestAnimationFrame(() => {
|
|
59
|
+
this.gestureHelper = new GestureHelper(this.svg)
|
|
60
|
+
})
|
|
80
61
|
}
|
|
81
62
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
this.startPanning(event)
|
|
63
|
+
disconnectedCallback(): void {
|
|
64
|
+
super.disconnectedCallback()
|
|
86
65
|
|
|
87
|
-
this.
|
|
88
|
-
this.
|
|
66
|
+
this.gestureHelper?.dispose()
|
|
67
|
+
this.gestureHelper = null
|
|
89
68
|
}
|
|
90
69
|
|
|
91
|
-
protected
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
116
|
-
|
|
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
|
|
130
|
-
|
|
81
|
+
protected onImageLoad(event: Event) {
|
|
82
|
+
const img = new Image()
|
|
131
83
|
|
|
132
|
-
|
|
133
|
-
const
|
|
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
|
-
|
|
87
|
+
if (!this.initialViewBox) {
|
|
88
|
+
const width = img.naturalWidth
|
|
89
|
+
const height = img.naturalHeight
|
|
140
90
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
this.
|
|
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
|
|
150
|
-
|
|
101
|
+
protected async updateImage() {
|
|
102
|
+
if (!this.imageProvider) {
|
|
103
|
+
throw new Error('ImageProvider is not set')
|
|
104
|
+
}
|
|
151
105
|
|
|
152
|
-
|
|
153
|
-
this.
|
|
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.
|
|
157
|
-
this.svg.removeEventListener('touchend', this.onZoomTouchEnd)
|
|
114
|
+
this.currentImage = await this.imageProvider.getImage(options)
|
|
158
115
|
}
|
|
159
116
|
|
|
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
|
|
117
|
+
protected onSvgWheel(event: WheelEvent) {
|
|
118
|
+
event.preventDefault()
|
|
167
119
|
|
|
168
|
-
|
|
169
|
-
|
|
120
|
+
const { offsetX, offsetY, deltaY } = event
|
|
121
|
+
const scale = deltaY > 0 ? 1 - this.zoomFactor : 1 + this.zoomFactor
|
|
170
122
|
|
|
171
|
-
|
|
172
|
-
this.resetZoom()
|
|
123
|
+
this.zoom(scale, offsetX, offsetY)
|
|
173
124
|
}
|
|
174
125
|
|
|
175
|
-
protected
|
|
176
|
-
|
|
177
|
-
this.zoomLevel = 1
|
|
178
|
-
}
|
|
126
|
+
protected onSvgPinch(e: GestureEventPinch) {
|
|
127
|
+
e.stopPropagation()
|
|
179
128
|
|
|
180
|
-
|
|
181
|
-
const { x, y } = this.getPointFromEvent(event)
|
|
129
|
+
const { scale, centerX, centerY } = e.detail
|
|
182
130
|
|
|
183
|
-
|
|
184
|
-
this.panStart = { x, y }
|
|
185
|
-
this.panInitiated = false // 팬 동작이 아직 시작되지 않았음을 나타냄
|
|
186
|
-
this.svg.classList.add('dragging')
|
|
131
|
+
console.log('pinch', e.detail)
|
|
187
132
|
|
|
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
|
-
}
|
|
133
|
+
this.zoom(scale, centerX, centerY)
|
|
195
134
|
}
|
|
196
135
|
|
|
197
|
-
protected
|
|
198
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
if (!this.panInitiated && Math.sqrt(dx * dx + dy * dy) < this.panThreshold) {
|
|
209
|
-
return
|
|
210
|
-
}
|
|
139
|
+
this.reset()
|
|
140
|
+
}
|
|
211
141
|
|
|
212
|
-
|
|
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 -
|
|
220
|
-
const newVbY = vbY -
|
|
152
|
+
const newVbX = vbX - deltaX * scale
|
|
153
|
+
const newVbY = vbY - deltaY * scale
|
|
221
154
|
|
|
222
155
|
this.viewBox = `${newVbX} ${newVbY} ${vbWidth} ${vbHeight}`
|
|
223
|
-
|
|
156
|
+
|
|
157
|
+
this.debouncedUpdateImage()
|
|
224
158
|
}
|
|
225
159
|
|
|
226
|
-
|
|
227
|
-
|
|
160
|
+
zoom(scale: number, offsetX: number, offsetY: number) {
|
|
161
|
+
if (Math.abs(scale) > this.zoomThreshold) {
|
|
162
|
+
this.zoomLevel *= scale
|
|
228
163
|
|
|
229
|
-
|
|
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
|
-
|
|
232
|
-
|
|
168
|
+
const newVbWidth = vbWidth / scale
|
|
169
|
+
const newVbHeight = vbHeight / scale
|
|
233
170
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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 |
|
|
273
|
-
if ('
|
|
216
|
+
protected getPointFromEvent(event: MouseEvent | PointerEvent | GestureEventDrag) {
|
|
217
|
+
if ('clientX' in event) {
|
|
274
218
|
return {
|
|
275
|
-
x: event.
|
|
276
|
-
y: event.
|
|
219
|
+
x: event.clientX,
|
|
220
|
+
y: event.clientY
|
|
277
221
|
}
|
|
278
222
|
} else {
|
|
223
|
+
const { clientX, clientY } = event.detail
|
|
279
224
|
return {
|
|
280
|
-
x:
|
|
281
|
-
y:
|
|
225
|
+
x: clientX,
|
|
226
|
+
y: clientY
|
|
282
227
|
}
|
|
283
228
|
}
|
|
284
229
|
}
|
|
@@ -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>
|