@operato/image-marker 8.0.0-alpha.10 → 8.0.0-alpha.18
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/ox-image-marker.d.ts +7 -0
- package/dist/src/ox-image-marker.js +221 -127
- package/dist/src/ox-image-marker.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/ox-image-marker.ts +250 -133
package/src/ox-image-marker.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import './ox-image-marker-mode'
|
|
2
2
|
|
|
3
3
|
import { html, css, svg, nothing } from 'lit'
|
|
4
|
-
import {
|
|
4
|
+
import { state, customElement } from 'lit/decorators.js'
|
|
5
5
|
import { ifDefined } from 'lit/directives/if-defined.js'
|
|
6
6
|
import { keyed } from 'lit/directives/keyed.js'
|
|
7
7
|
|
|
@@ -50,6 +50,32 @@ export class OxImageMarker extends BaseImageMarker {
|
|
|
50
50
|
private offsetX = 0
|
|
51
51
|
private offsetY = 0
|
|
52
52
|
|
|
53
|
+
connectedCallback() {
|
|
54
|
+
super.connectedCallback()
|
|
55
|
+
// 키다운 이벤트 리스너 추가
|
|
56
|
+
window.addEventListener('keydown', this.handleKeyDown)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
disconnectedCallback() {
|
|
60
|
+
super.disconnectedCallback()
|
|
61
|
+
// 키다운 이벤트 리스너 제거
|
|
62
|
+
window.removeEventListener('keydown', this.handleKeyDown)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private handleKeyDown = (event: KeyboardEvent) => {
|
|
66
|
+
if ((event.key === 'Delete' || event.key === 'Backspace') && this.selectedShape) {
|
|
67
|
+
this.deleteSelectedShape()
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private deleteSelectedShape() {
|
|
72
|
+
if (this.selectedShape) {
|
|
73
|
+
this.shapes = this.shapes.filter(s => s.id !== this.selectedShape!.id)
|
|
74
|
+
this.selectedShape = null
|
|
75
|
+
this.requestUpdate()
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
53
79
|
render() {
|
|
54
80
|
return html`
|
|
55
81
|
<div class="container">
|
|
@@ -71,12 +97,7 @@ export class OxImageMarker extends BaseImageMarker {
|
|
|
71
97
|
const { mode, shapeType } = e.detail
|
|
72
98
|
this.setMode(mode, shapeType)
|
|
73
99
|
}}
|
|
74
|
-
@clicked-delete=${() =>
|
|
75
|
-
if (this.selectedShape) {
|
|
76
|
-
this.shapes = this.shapes.filter(s => s.id !== this.selectedShape!.id)
|
|
77
|
-
this.requestUpdate()
|
|
78
|
-
}
|
|
79
|
-
}}
|
|
100
|
+
@clicked-delete=${() => this.deleteSelectedShape()}
|
|
80
101
|
@clicked-save=${() => {
|
|
81
102
|
this.dispatchEvent(
|
|
82
103
|
new CustomEvent('shapes-changed', {
|
|
@@ -125,78 +146,25 @@ export class OxImageMarker extends BaseImageMarker {
|
|
|
125
146
|
}
|
|
126
147
|
|
|
127
148
|
private startDrawing(event: MouseEvent) {
|
|
128
|
-
this.drawing = true
|
|
129
149
|
const { x, y } = this.getSvgPoint(event)
|
|
130
|
-
this.startX = x
|
|
131
|
-
this.startY = y
|
|
132
150
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
text: 'ABC'
|
|
142
|
-
}
|
|
143
|
-
break
|
|
144
|
-
case 'rectangle':
|
|
145
|
-
newShape = {
|
|
146
|
-
id: Date.now().toString(),
|
|
147
|
-
type: 'rectangle',
|
|
148
|
-
x: this.startX,
|
|
149
|
-
y: this.startY,
|
|
150
|
-
width: 0,
|
|
151
|
-
height: 0
|
|
152
|
-
}
|
|
153
|
-
break
|
|
154
|
-
case 'circle':
|
|
155
|
-
newShape = {
|
|
156
|
-
id: Date.now().toString(),
|
|
157
|
-
type: 'circle',
|
|
158
|
-
cx: this.startX,
|
|
159
|
-
cy: this.startY,
|
|
160
|
-
r: 0
|
|
161
|
-
}
|
|
162
|
-
break
|
|
163
|
-
case 'triangle':
|
|
164
|
-
newShape = {
|
|
165
|
-
id: Date.now().toString(),
|
|
166
|
-
type: 'triangle',
|
|
167
|
-
x1: this.startX,
|
|
168
|
-
y1: this.startY,
|
|
169
|
-
width: 0,
|
|
170
|
-
height: 0,
|
|
171
|
-
points: `${this.startX},${this.startY} ${this.startX},${this.startY} ${this.startX},${this.startY}`
|
|
172
|
-
}
|
|
173
|
-
break
|
|
174
|
-
case 'line':
|
|
175
|
-
newShape = {
|
|
176
|
-
id: Date.now().toString(),
|
|
177
|
-
type: 'line',
|
|
178
|
-
x1: this.startX,
|
|
179
|
-
y1: this.startY,
|
|
180
|
-
width: 0,
|
|
181
|
-
height: 0
|
|
182
|
-
}
|
|
183
|
-
break
|
|
184
|
-
case 'arrow':
|
|
185
|
-
newShape = {
|
|
186
|
-
id: Date.now().toString(),
|
|
187
|
-
type: 'arrow',
|
|
188
|
-
x1: this.startX,
|
|
189
|
-
y1: this.startY,
|
|
190
|
-
width: 0,
|
|
191
|
-
height: 0
|
|
192
|
-
}
|
|
193
|
-
break
|
|
194
|
-
default:
|
|
195
|
-
return
|
|
196
|
-
}
|
|
151
|
+
if (this.currentShapeType === 'text') {
|
|
152
|
+
const textElement = document.createElementNS('http://www.w3.org/2000/svg', 'text')
|
|
153
|
+
|
|
154
|
+
textElement.setAttribute('x', x.toString())
|
|
155
|
+
textElement.setAttribute('y', y.toString())
|
|
156
|
+
textElement.textContent = 'ABC'
|
|
157
|
+
|
|
158
|
+
this.svg.appendChild(textElement)
|
|
197
159
|
|
|
198
|
-
|
|
199
|
-
|
|
160
|
+
requestAnimationFrame(() => {
|
|
161
|
+
this.showTextInput(textElement, null, { x, y })
|
|
162
|
+
})
|
|
163
|
+
} else {
|
|
164
|
+
this.startX = x
|
|
165
|
+
this.startY = y
|
|
166
|
+
this.drawing = true
|
|
167
|
+
}
|
|
200
168
|
}
|
|
201
169
|
|
|
202
170
|
private setMode(mode: ImageMarkerMode, shapeType: ShapeType) {
|
|
@@ -204,52 +172,120 @@ export class OxImageMarker extends BaseImageMarker {
|
|
|
204
172
|
this.currentShapeType = shapeType
|
|
205
173
|
}
|
|
206
174
|
|
|
207
|
-
// 마우스 이동 이벤트 핸들러 (도형 그리기 중)
|
|
208
175
|
private onMouseMove = (event: MouseEvent) => {
|
|
209
|
-
if (!this.drawing
|
|
176
|
+
if (!this.drawing) return
|
|
177
|
+
|
|
210
178
|
const { x, y } = this.getSvgPoint(event)
|
|
211
179
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
180
|
+
const minDistance = 5
|
|
181
|
+
const distance = Math.sqrt(Math.pow(x - this.startX, 2) + Math.pow(y - this.startY, 2))
|
|
182
|
+
|
|
183
|
+
if (!this.selectedShape && distance >= minDistance) {
|
|
184
|
+
let newShape: Shape
|
|
185
|
+
|
|
186
|
+
switch (this.currentShapeType) {
|
|
187
|
+
case 'rectangle':
|
|
188
|
+
newShape = {
|
|
189
|
+
id: Date.now().toString(),
|
|
190
|
+
type: 'rectangle',
|
|
191
|
+
x: this.startX,
|
|
192
|
+
y: this.startY,
|
|
193
|
+
width: 0,
|
|
194
|
+
height: 0
|
|
195
|
+
}
|
|
196
|
+
break
|
|
197
|
+
case 'circle':
|
|
198
|
+
newShape = {
|
|
199
|
+
id: Date.now().toString(),
|
|
200
|
+
type: 'circle',
|
|
201
|
+
cx: this.startX,
|
|
202
|
+
cy: this.startY,
|
|
203
|
+
r: 0
|
|
204
|
+
}
|
|
205
|
+
break
|
|
206
|
+
case 'triangle':
|
|
207
|
+
newShape = {
|
|
208
|
+
id: Date.now().toString(),
|
|
209
|
+
type: 'triangle',
|
|
210
|
+
x1: this.startX,
|
|
211
|
+
y1: this.startY,
|
|
212
|
+
width: 0,
|
|
213
|
+
height: 0,
|
|
214
|
+
points: `${this.startX},${this.startY} ${this.startX},${this.startY} ${this.startX},${this.startY}`
|
|
215
|
+
}
|
|
216
|
+
break
|
|
217
|
+
case 'line':
|
|
218
|
+
newShape = {
|
|
219
|
+
id: Date.now().toString(),
|
|
220
|
+
type: 'line',
|
|
221
|
+
x1: this.startX,
|
|
222
|
+
y1: this.startY,
|
|
223
|
+
width: 0,
|
|
224
|
+
height: 0
|
|
225
|
+
}
|
|
226
|
+
break
|
|
227
|
+
case 'arrow':
|
|
228
|
+
newShape = {
|
|
229
|
+
id: Date.now().toString(),
|
|
230
|
+
type: 'arrow',
|
|
231
|
+
x1: this.startX,
|
|
232
|
+
y1: this.startY,
|
|
233
|
+
width: 0,
|
|
234
|
+
height: 0
|
|
235
|
+
}
|
|
236
|
+
break
|
|
237
|
+
default:
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
this.selectedShape = newShape
|
|
242
|
+
this.shapes = [...this.shapes, newShape]
|
|
236
243
|
}
|
|
237
244
|
|
|
238
|
-
this.
|
|
245
|
+
if (this.selectedShape) {
|
|
246
|
+
switch (this.selectedShape.type) {
|
|
247
|
+
case 'rectangle':
|
|
248
|
+
this.selectedShape.x = Math.min(x, this.startX)
|
|
249
|
+
this.selectedShape.y = Math.min(y, this.startY)
|
|
250
|
+
this.selectedShape.width = Math.abs(x - this.startX)
|
|
251
|
+
this.selectedShape.height = Math.abs(y - this.startY)
|
|
252
|
+
break
|
|
253
|
+
case 'circle':
|
|
254
|
+
const dx = x - this.selectedShape.cx!
|
|
255
|
+
const dy = y - this.selectedShape.cy!
|
|
256
|
+
this.selectedShape.r = Math.sqrt(dx * dx + dy * dy)
|
|
257
|
+
break
|
|
258
|
+
case 'triangle':
|
|
259
|
+
const x2 = x
|
|
260
|
+
const y2 = y
|
|
261
|
+
const x3 = this.startX - (x - this.startX)
|
|
262
|
+
const y3 = y
|
|
263
|
+
this.selectedShape.points = `${this.startX},${this.startY} ${x2},${y2} ${x3},${y3}`
|
|
264
|
+
break
|
|
265
|
+
case 'line':
|
|
266
|
+
case 'arrow':
|
|
267
|
+
this.selectedShape.x2 = x
|
|
268
|
+
this.selectedShape.y2 = y
|
|
269
|
+
break
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
this.requestUpdate()
|
|
273
|
+
}
|
|
239
274
|
}
|
|
240
275
|
|
|
241
|
-
// 마우스 업 이벤트 핸들러 (도형 그리기 종료)
|
|
242
276
|
private onMouseUp = () => {
|
|
243
277
|
this.drawing = false
|
|
244
278
|
const svg = this.svg as SVGSVGElement
|
|
245
279
|
svg.removeEventListener('mousemove', this.onMouseMove)
|
|
246
280
|
svg.removeEventListener('mouseup', this.onMouseUp)
|
|
247
281
|
|
|
248
|
-
this.selectedShape
|
|
282
|
+
if (this.selectedShape) {
|
|
283
|
+
this.selectedShape.selected = true
|
|
284
|
+
}
|
|
285
|
+
|
|
249
286
|
this.requestUpdate()
|
|
250
287
|
}
|
|
251
288
|
|
|
252
|
-
// SVG 좌표계로 마우스 좌표 변환
|
|
253
289
|
private getSvgPoint(event: MouseEvent) {
|
|
254
290
|
const svg = this.svg
|
|
255
291
|
const point = svg.createSVGPoint()
|
|
@@ -259,11 +295,10 @@ export class OxImageMarker extends BaseImageMarker {
|
|
|
259
295
|
return point.matrixTransform(ctm)
|
|
260
296
|
}
|
|
261
297
|
|
|
262
|
-
// 도형 렌더링 함수
|
|
263
298
|
protected renderShape(shape: Shape) {
|
|
264
299
|
const commonAttrs = {
|
|
265
300
|
stroke: shape.selected ? 'blue' : 'red',
|
|
266
|
-
'stroke-width':
|
|
301
|
+
'stroke-width': 1,
|
|
267
302
|
fill: 'transparent',
|
|
268
303
|
cursor: 'pointer'
|
|
269
304
|
}
|
|
@@ -271,12 +306,11 @@ export class OxImageMarker extends BaseImageMarker {
|
|
|
271
306
|
switch (shape.type) {
|
|
272
307
|
case 'text':
|
|
273
308
|
return svg`
|
|
274
|
-
|
|
275
309
|
<text
|
|
276
310
|
x="${shape.x!}"
|
|
277
311
|
y="${shape.y!}"
|
|
278
312
|
fill="${commonAttrs.stroke}"
|
|
279
|
-
style="cursor: ${commonAttrs.cursor}; font-size: 16px;
|
|
313
|
+
style="cursor: ${commonAttrs.cursor}; font-size: 16px;"
|
|
280
314
|
@mousedown="${(e: MouseEvent) => this.onShapeMouseDown(e, shape)}"
|
|
281
315
|
@dblclick="${(e: MouseEvent) => this.onTextDoubleClick(e, shape)}"
|
|
282
316
|
>${shape.text}</text>
|
|
@@ -377,7 +411,6 @@ export class OxImageMarker extends BaseImageMarker {
|
|
|
377
411
|
|
|
378
412
|
this.selectedShape = this.shapes.find(s => s.id == shape.id)!
|
|
379
413
|
|
|
380
|
-
// 마우스 클릭 좌표와 도형의 현재 좌표 차이 계산
|
|
381
414
|
const { x, y } = this.getSvgPoint(event)
|
|
382
415
|
|
|
383
416
|
switch (shape.type) {
|
|
@@ -404,20 +437,16 @@ export class OxImageMarker extends BaseImageMarker {
|
|
|
404
437
|
break
|
|
405
438
|
}
|
|
406
439
|
|
|
407
|
-
// 드래그 이벤트 리스너 추가
|
|
408
440
|
const svg = this.svg as SVGSVGElement
|
|
409
441
|
svg.addEventListener('mousemove', this.onShapeMouseMove)
|
|
410
442
|
svg.addEventListener('mouseup', this.onShapeMouseUp)
|
|
411
443
|
}
|
|
412
444
|
|
|
413
445
|
private onShapeMouseMove = (event: MouseEvent) => {
|
|
414
|
-
if (!this.selectedShape)
|
|
415
|
-
return
|
|
416
|
-
}
|
|
446
|
+
if (!this.selectedShape) return
|
|
417
447
|
|
|
418
448
|
const { x, y } = this.getSvgPoint(event)
|
|
419
449
|
|
|
420
|
-
// 마우스 이동에 따라 도형의 위치 업데이트
|
|
421
450
|
switch (this.selectedShape.type) {
|
|
422
451
|
case 'text':
|
|
423
452
|
case 'rectangle':
|
|
@@ -449,7 +478,7 @@ export class OxImageMarker extends BaseImageMarker {
|
|
|
449
478
|
break
|
|
450
479
|
}
|
|
451
480
|
|
|
452
|
-
this.requestUpdate()
|
|
481
|
+
this.requestUpdate()
|
|
453
482
|
}
|
|
454
483
|
|
|
455
484
|
private onShapeMouseUp = () => {
|
|
@@ -459,9 +488,7 @@ export class OxImageMarker extends BaseImageMarker {
|
|
|
459
488
|
}
|
|
460
489
|
|
|
461
490
|
private getScale(): number {
|
|
462
|
-
if (!this.svg)
|
|
463
|
-
return 1
|
|
464
|
-
}
|
|
491
|
+
if (!this.svg) return 1
|
|
465
492
|
|
|
466
493
|
const [vbWidth, vbHeight] = this.viewBox.split(' ').map(Number)
|
|
467
494
|
|
|
@@ -593,34 +620,124 @@ export class OxImageMarker extends BaseImageMarker {
|
|
|
593
620
|
private onTextDoubleClick(event: MouseEvent, shape: Shape) {
|
|
594
621
|
event.stopPropagation()
|
|
595
622
|
|
|
596
|
-
|
|
623
|
+
const textElement = event.target as SVGTextElement
|
|
624
|
+
|
|
625
|
+
this.showTextInput(textElement, shape)
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
private showTextInput(textElement: SVGTextElement, shape: Shape | null = null, options?: { x: number; y: number }) {
|
|
629
|
+
const input: HTMLInputElement = document.createElement('input')
|
|
597
630
|
input.style.position = 'absolute'
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
631
|
+
|
|
632
|
+
const ctm = textElement.getScreenCTM() || { a: 1, e: 0, d: 1, f: 0 }
|
|
633
|
+
const bbox = textElement.getBBox()
|
|
634
|
+
|
|
635
|
+
const screenX = bbox.x * ctm.a + ctm.e
|
|
636
|
+
const screenY = bbox.y * ctm.d + ctm.f
|
|
637
|
+
|
|
638
|
+
input.style.left = `${screenX}px`
|
|
639
|
+
input.style.top = `${screenY}px`
|
|
640
|
+
|
|
641
|
+
const computedStyle = window.getComputedStyle(textElement)
|
|
642
|
+
const fontSize = parseFloat(computedStyle.fontSize) * ctm.a
|
|
643
|
+
const fontFamily = computedStyle.fontFamily
|
|
644
|
+
const fontWeight = computedStyle.fontWeight
|
|
645
|
+
const textColor = computedStyle.fill || computedStyle.color
|
|
646
|
+
const lineHeight = parseFloat(computedStyle.lineHeight) * ctm.d
|
|
647
|
+
|
|
648
|
+
input.style.fontSize = `${fontSize}px`
|
|
649
|
+
input.style.fontFamily = fontFamily
|
|
650
|
+
input.style.fontWeight = fontWeight
|
|
651
|
+
input.style.color = textColor
|
|
652
|
+
input.style.lineHeight = `${lineHeight}px`
|
|
653
|
+
input.style.padding = '0'
|
|
654
|
+
input.style.margin = '0'
|
|
655
|
+
input.style.border = 'none'
|
|
656
|
+
input.style.outline = 'none'
|
|
657
|
+
input.style.background = 'transparent'
|
|
658
|
+
|
|
659
|
+
if (shape) {
|
|
660
|
+
input.value = shape.text || ''
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
textElement.style.visibility = 'hidden'
|
|
664
|
+
|
|
601
665
|
document.body.appendChild(input)
|
|
602
|
-
|
|
666
|
+
this.adjustInputWidth(input)
|
|
667
|
+
requestAnimationFrame(() => input.focus())
|
|
668
|
+
|
|
669
|
+
input.addEventListener('input', () => {
|
|
670
|
+
this.adjustInputWidth(input)
|
|
671
|
+
})
|
|
603
672
|
|
|
604
673
|
input.addEventListener('keydown', e => {
|
|
605
674
|
if (e.key === 'Enter') {
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
675
|
+
const textValue = input.value.trim()
|
|
676
|
+
|
|
677
|
+
if (textValue) {
|
|
678
|
+
if (shape) {
|
|
679
|
+
shape.text = textValue
|
|
680
|
+
} else {
|
|
681
|
+
const newShape: Shape = {
|
|
682
|
+
id: Date.now().toString(),
|
|
683
|
+
type: 'text',
|
|
684
|
+
x: options!.x,
|
|
685
|
+
y: options!.y,
|
|
686
|
+
text: textValue
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
this.shapes = [...this.shapes, newShape]
|
|
690
|
+
}
|
|
691
|
+
} else if (shape) {
|
|
692
|
+
this.shapes = this.shapes.filter(s => s.id !== shape.id)
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
document.body.removeChild(input)
|
|
696
|
+
|
|
697
|
+
if (!shape) {
|
|
698
|
+
this.svg.removeChild(textElement)
|
|
699
|
+
} else {
|
|
700
|
+
textElement.style.visibility = 'visible'
|
|
701
|
+
}
|
|
609
702
|
|
|
610
703
|
this.requestUpdate()
|
|
611
704
|
} else if (e.key === 'Escape') {
|
|
612
|
-
document.body.removeChild(input
|
|
613
|
-
input = null
|
|
705
|
+
document.body.removeChild(input)
|
|
614
706
|
|
|
615
|
-
|
|
707
|
+
if (!shape) {
|
|
708
|
+
this.svg.removeChild(textElement)
|
|
709
|
+
} else {
|
|
710
|
+
textElement.style.visibility = 'visible'
|
|
711
|
+
}
|
|
616
712
|
}
|
|
617
713
|
})
|
|
618
714
|
|
|
619
715
|
input.addEventListener('blur', () => {
|
|
620
716
|
requestAnimationFrame(() => {
|
|
621
|
-
|
|
622
|
-
|
|
717
|
+
if (!shape) {
|
|
718
|
+
this.svg.removeChild(textElement)
|
|
719
|
+
} else {
|
|
720
|
+
textElement.style.visibility = 'visible'
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
if (input.parentElement) {
|
|
724
|
+
document.body.removeChild(input)
|
|
725
|
+
}
|
|
623
726
|
})
|
|
624
727
|
})
|
|
625
728
|
}
|
|
729
|
+
|
|
730
|
+
private adjustInputWidth(input: HTMLInputElement) {
|
|
731
|
+
const minWidth = 50
|
|
732
|
+
const textWidth = this.measureTextWidth(input.value || input.placeholder, input)
|
|
733
|
+
input.style.width = `${Math.max(textWidth + 10, minWidth)}px`
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
private measureTextWidth(text: string, input: HTMLInputElement): number {
|
|
737
|
+
const canvas = document.createElement('canvas')
|
|
738
|
+
const context = canvas.getContext('2d')!
|
|
739
|
+
context.font = window.getComputedStyle(input).font
|
|
740
|
+
|
|
741
|
+
return context.measureText(text).width
|
|
742
|
+
}
|
|
626
743
|
}
|