@live-change/image-frontend 0.0.3
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/LICENSE +21 -0
- package/front/index.html +11 -0
- package/front/public/favicon.ico +0 -0
- package/front/public/images/empty-photo.svg +38 -0
- package/front/public/images/empty-user-photo.svg +33 -0
- package/front/public/images/logo.svg +34 -0
- package/front/public/images/logo128.png +0 -0
- package/front/src/App.vue +32 -0
- package/front/src/EditorTest.vue +28 -0
- package/front/src/Image.vue +137 -0
- package/front/src/ImageCrop.vue +382 -0
- package/front/src/ImageEditor.vue +211 -0
- package/front/src/ImageUpload.js +132 -0
- package/front/src/NavBar.vue +105 -0
- package/front/src/UploadTest.vue +57 -0
- package/front/src/dom.js +82 -0
- package/front/src/entry-client.js +6 -0
- package/front/src/entry-server.js +6 -0
- package/front/src/imageResizer.js +5 -0
- package/front/src/imageUploads.js +29 -0
- package/front/src/imageUtils.js +438 -0
- package/front/src/preprocessImageFile.js +122 -0
- package/front/src/router.js +41 -0
- package/front/vite.config.js +18 -0
- package/index.js +7 -0
- package/package.json +76 -0
- package/server/init.js +5 -0
- package/server/services.config.js +17 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="w-full relative surface-400 overflow-hidden" ref="dragArea"
|
|
3
|
+
@mousedown="handleEditorMouseDown"
|
|
4
|
+
@mouseup="handleEditorMouseUp"
|
|
5
|
+
@mouseout="handleEditorMouseOut"
|
|
6
|
+
@mousemove="handleEditorMouseMove"
|
|
7
|
+
@touchstart="handleEditorTouchStart"
|
|
8
|
+
@touchend="handleEditorTouchEnd"
|
|
9
|
+
@touchcancel="handleEditorTouchCancel"
|
|
10
|
+
@touchmove="handleEditorTouchMove"
|
|
11
|
+
@wheel="handleEditorWheel">
|
|
12
|
+
<div class="p-6 md:p-8 lg:p-8">
|
|
13
|
+
<div class="w-auto relative"
|
|
14
|
+
:style="`aspect-ratio: ${aspectRatio}`" ref="cropArea">
|
|
15
|
+
<div class="absolute left-50 top-50 w-0 h-0">
|
|
16
|
+
<Image :image="sourceImage"
|
|
17
|
+
:style="{ width: `${rectSize?.x}px`, transform: imageTransform }"
|
|
18
|
+
@size="handleImageSize" />
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
<canvas class="absolute w-full h-full left-0 top-0 pointer-events-none" ref="overlayCanvas" />
|
|
23
|
+
</div>
|
|
24
|
+
</template>
|
|
25
|
+
|
|
26
|
+
<script setup>
|
|
27
|
+
|
|
28
|
+
import Image from "./Image.vue"
|
|
29
|
+
|
|
30
|
+
import { ref, reactive, watch, computed, onMounted, defineEmits, defineExpose } from 'vue'
|
|
31
|
+
import { toRefs, useResizeObserver, useDebounceFn } from '@vueuse/core'
|
|
32
|
+
import { getElementPositionInElement, getElementPositionInWindow } from "./dom"
|
|
33
|
+
import { loadImage, imageToCanvas } from "./imageUtils.js"
|
|
34
|
+
|
|
35
|
+
const props = defineProps({
|
|
36
|
+
sourceImage: { // image
|
|
37
|
+
type: String,
|
|
38
|
+
required: true
|
|
39
|
+
},
|
|
40
|
+
sourceUpload: {
|
|
41
|
+
type: Object,
|
|
42
|
+
default: null
|
|
43
|
+
},
|
|
44
|
+
aspectRatio: {
|
|
45
|
+
type: Number,
|
|
46
|
+
default: 1
|
|
47
|
+
},
|
|
48
|
+
fill: {
|
|
49
|
+
type: Boolean,
|
|
50
|
+
default: true
|
|
51
|
+
},
|
|
52
|
+
ready: {
|
|
53
|
+
},
|
|
54
|
+
type: {
|
|
55
|
+
type: String,
|
|
56
|
+
default: 'rect' // or 'circle'
|
|
57
|
+
},
|
|
58
|
+
crop: {
|
|
59
|
+
type: Object,
|
|
60
|
+
default: () => ({ x: 0, y: 0, zoom:1, orientation: 0 })
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const emit = defineEmits(['update:ready'])
|
|
65
|
+
|
|
66
|
+
const { sourceImage, aspectRatio, fill, sourceUpload } = toRefs(props)
|
|
67
|
+
|
|
68
|
+
const overlayCanvas = ref()
|
|
69
|
+
const cropArea = ref()
|
|
70
|
+
const dragArea = ref()
|
|
71
|
+
|
|
72
|
+
const rectSize = ref()
|
|
73
|
+
|
|
74
|
+
function repaintOverlayCanvas() {
|
|
75
|
+
console.log('canvas', overlayCanvas)
|
|
76
|
+
const canvas = overlayCanvas.value
|
|
77
|
+
const content = cropArea.value
|
|
78
|
+
const editor = dragArea.value
|
|
79
|
+
if(!canvas) return
|
|
80
|
+
if(!content) return
|
|
81
|
+
if(!editor) return
|
|
82
|
+
const pixelRatio = window.devicePixelRatio || 1
|
|
83
|
+
if((canvas.width != Math.floor(canvas.clientWidth * pixelRatio))
|
|
84
|
+
|| (canvas.height != Math.floor(canvas.clientHeight * pixelRatio))) {
|
|
85
|
+
canvas.width = Math.floor(canvas.clientWidth * pixelRatio)
|
|
86
|
+
canvas.height = Math.floor(canvas.clientHeight * pixelRatio)
|
|
87
|
+
}
|
|
88
|
+
const position = getElementPositionInElement(content, editor)
|
|
89
|
+
const size = { x: content.clientWidth, y: content.clientHeight }
|
|
90
|
+
rectSize.value = size
|
|
91
|
+
const context = canvas.getContext('2d')
|
|
92
|
+
context.clearRect(0, 0, canvas.width, canvas.height)
|
|
93
|
+
context.fillStyle = 'rgba(0, 0, 0, 0.5)'
|
|
94
|
+
context.fillRect(0, 0, canvas.width, canvas.height)
|
|
95
|
+
context.strokeStyle = "white"
|
|
96
|
+
context.lineWidth = 1.5 * pixelRatio;
|
|
97
|
+
if(props.type == 'circle') {
|
|
98
|
+
context.save()
|
|
99
|
+
context.globalCompositeOperation = 'destination-out'
|
|
100
|
+
context.fillStyle = '#000'
|
|
101
|
+
context.beginPath()
|
|
102
|
+
context.ellipse(
|
|
103
|
+
(position.x + size.x / 2) * pixelRatio,
|
|
104
|
+
(position.y + size.y / 2) * pixelRatio,
|
|
105
|
+
(size.x / 2) * pixelRatio,
|
|
106
|
+
(size.y / 2) * pixelRatio,
|
|
107
|
+
0, 0, 2 * Math.PI
|
|
108
|
+
)
|
|
109
|
+
context.fill()
|
|
110
|
+
context.restore()
|
|
111
|
+
context.beginPath()
|
|
112
|
+
context.ellipse(
|
|
113
|
+
(position.x + size.x / 2) * pixelRatio,
|
|
114
|
+
(position.y + size.y / 2) * pixelRatio,
|
|
115
|
+
(size.x / 2) * pixelRatio,
|
|
116
|
+
(size.y / 2) * pixelRatio,
|
|
117
|
+
0, 0, 2 * Math.PI
|
|
118
|
+
)
|
|
119
|
+
context.stroke()
|
|
120
|
+
} else {
|
|
121
|
+
context.clearRect(position.x * pixelRatio, position.y * pixelRatio,
|
|
122
|
+
size.x * pixelRatio, size.y * pixelRatio)
|
|
123
|
+
context.strokeRect(position.x * pixelRatio, position.y * pixelRatio,
|
|
124
|
+
size.x * pixelRatio, size.y * pixelRatio)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const repaintDebounced = useDebounceFn(() => repaintOverlayCanvas(), 300)
|
|
129
|
+
|
|
130
|
+
useResizeObserver(overlayCanvas, (entries) => repaintDebounced())
|
|
131
|
+
watch(() => [props.width, props.height, props.type], () => repaintDebounced())
|
|
132
|
+
|
|
133
|
+
const position = reactive({ x: props.crop.x, y: props.crop.y, scale: props.crop.zoom })
|
|
134
|
+
const imageTransform = computed(() => {
|
|
135
|
+
const x = - position.x * 50
|
|
136
|
+
const y = - position.y * 50
|
|
137
|
+
const s = position.scale
|
|
138
|
+
return `translate(-50%, -50%) scale(${s}) translate(${x}%, ${y}%)`
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
const sourceImageSize = ref()
|
|
142
|
+
function handleImageSize(size) {
|
|
143
|
+
console.log("SIZE EVENT", size)
|
|
144
|
+
sourceImageSize.value = size
|
|
145
|
+
}
|
|
146
|
+
const sourceAspectRatio = computed(() => sourceImageSize.value?.width / sourceImageSize.value?.height)
|
|
147
|
+
|
|
148
|
+
const minScale = computed(() => {
|
|
149
|
+
if(!sourceImageSize.value) return 0.01
|
|
150
|
+
const imageRatio = sourceAspectRatio.value
|
|
151
|
+
const requiredRatio = aspectRatio.value
|
|
152
|
+
if(fill.value) {
|
|
153
|
+
return (imageRatio > requiredRatio)
|
|
154
|
+
? (imageRatio / requiredRatio)
|
|
155
|
+
: 1
|
|
156
|
+
} else {
|
|
157
|
+
return (imageRatio > requiredRatio)
|
|
158
|
+
? 1
|
|
159
|
+
: imageRatio / requiredRatio
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
const maxScale = ref(5)
|
|
163
|
+
|
|
164
|
+
function updatePosition(x, y, scale) {
|
|
165
|
+
if(scale > maxScale.value) scale = maxScale.value
|
|
166
|
+
if(scale < minScale.value) scale = minScale.value
|
|
167
|
+
const ratio = aspectRatio.value / sourceAspectRatio.value
|
|
168
|
+
let xMin, xMax, yMin, yMax
|
|
169
|
+
if(fill.value) {
|
|
170
|
+
xMin = -1 + (1 / scale)
|
|
171
|
+
xMax = 1 - (1 / scale)
|
|
172
|
+
yMin = -1 + (1 / scale / ratio)
|
|
173
|
+
yMax = 1 - (1 / scale / ratio)
|
|
174
|
+
} else {
|
|
175
|
+
xMin = - 1
|
|
176
|
+
xMax = 1
|
|
177
|
+
yMin = - 1
|
|
178
|
+
yMax = 1
|
|
179
|
+
}
|
|
180
|
+
if(x < xMin) x = xMin
|
|
181
|
+
if(x > xMax) x = xMax
|
|
182
|
+
if(y < yMin) y = yMin
|
|
183
|
+
if(y > yMax) y = yMax
|
|
184
|
+
position.x = x
|
|
185
|
+
position.y = y
|
|
186
|
+
position.scale = scale
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
watch(() => [minScale.value, maxScale.value], () => updatePosition(position.x, position.y, position.scale))
|
|
190
|
+
|
|
191
|
+
const ready = computed(() => !!sourceImageSize.value)
|
|
192
|
+
console.log("R", ready.value)
|
|
193
|
+
if(ready.value) onMounted(() => emit('update:ready', true))
|
|
194
|
+
watch(() => ready.value, r => {
|
|
195
|
+
console.log("RR", ready.value)
|
|
196
|
+
emit('update:ready', r)
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
function preProcessTouch(ev, id) {
|
|
200
|
+
const contentPosition = getElementPositionInWindow(cropArea.value)
|
|
201
|
+
const contentSize = rectSize.value
|
|
202
|
+
const destX = (ev.clientX - contentPosition.x ) / contentSize.x * 2 - 1
|
|
203
|
+
const destY = (ev.clientY - contentPosition.y ) / contentSize.y * 2 - 1
|
|
204
|
+
const ratio = aspectRatio.value / sourceAspectRatio.value
|
|
205
|
+
const srcX = destX / position.scale
|
|
206
|
+
const srcY = destY / position.scale / ratio
|
|
207
|
+
//console.log(destX, destY, "R", ratio, "S", srcX, srcY)
|
|
208
|
+
return {
|
|
209
|
+
id,
|
|
210
|
+
x: srcX,
|
|
211
|
+
y: srcY,
|
|
212
|
+
dx: destX,
|
|
213
|
+
dy: destY
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const dragStart = ref()
|
|
218
|
+
|
|
219
|
+
function updateTouches(newTouches) {
|
|
220
|
+
if(!sourceImage.value) return
|
|
221
|
+
const ratio = aspectRatio.value / sourceAspectRatio.value
|
|
222
|
+
const newCenter = newTouches.reduce(
|
|
223
|
+
(a, b) => ({
|
|
224
|
+
x: a.x + b.x / newTouches.length,
|
|
225
|
+
y: a.y + b.y / newTouches.length,
|
|
226
|
+
dx: a.dx + b.dx / newTouches.length,
|
|
227
|
+
dy: a.dy + b.dy / newTouches.length
|
|
228
|
+
}), { x: 0, y: 0, dx:0, dy:0 })
|
|
229
|
+
const newSize = newTouches.length > 1
|
|
230
|
+
? newTouches.map(t => {
|
|
231
|
+
let x = t.dx - newCenter.dx, y = (t.dy - newCenter.dy)*ratio
|
|
232
|
+
return Math.sqrt(x*x+y*y)
|
|
233
|
+
}).reduce((a,b) => a + b / newTouches.length, 0)
|
|
234
|
+
: 1
|
|
235
|
+
if(newTouches.length == (dragStart.value && dragStart.value.touchCount || 0)) {
|
|
236
|
+
if(!newTouches.length) return
|
|
237
|
+
//console.log("newSize", newSize, "size", this.dragStart.size, "scale", this.dragStart.size * newSize)
|
|
238
|
+
updatePosition(
|
|
239
|
+
dragStart.value.x - newCenter.x,
|
|
240
|
+
dragStart.value.y - newCenter.y,
|
|
241
|
+
dragStart.value.size * newSize
|
|
242
|
+
)
|
|
243
|
+
} else {
|
|
244
|
+
if(newTouches.length) {
|
|
245
|
+
dragStart.value = {
|
|
246
|
+
x: position.x + newCenter.x,
|
|
247
|
+
y: position.y + newCenter.y,
|
|
248
|
+
size: position.scale / newSize,
|
|
249
|
+
touchCount: newTouches.length
|
|
250
|
+
}
|
|
251
|
+
} else {
|
|
252
|
+
dragStart.value = null
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function handleEditorMouseDown(ev) {
|
|
258
|
+
ev.preventDefault()
|
|
259
|
+
ev.stopPropagation()
|
|
260
|
+
if(dragArea.value && cropArea.value) updateTouches([ preProcessTouch(ev, 'mouse') ])
|
|
261
|
+
}
|
|
262
|
+
function handleEditorMouseUp(ev) {
|
|
263
|
+
ev.preventDefault()
|
|
264
|
+
ev.stopPropagation()
|
|
265
|
+
updateTouches([ ])
|
|
266
|
+
}
|
|
267
|
+
function handleEditorMouseOut(ev) {
|
|
268
|
+
ev.preventDefault()
|
|
269
|
+
ev.stopPropagation()
|
|
270
|
+
updateTouches([ ])
|
|
271
|
+
}
|
|
272
|
+
function handleEditorMouseMove(ev) {
|
|
273
|
+
ev.preventDefault()
|
|
274
|
+
ev.stopPropagation()
|
|
275
|
+
if(dragArea.value && cropArea.value && dragStart.value) updateTouches([ preProcessTouch(ev, 'mouse') ])
|
|
276
|
+
}
|
|
277
|
+
function handleEditorTouchStart(ev) {
|
|
278
|
+
ev.preventDefault()
|
|
279
|
+
ev.stopPropagation()
|
|
280
|
+
if(dragArea.value && cropArea.value) updateTouches(
|
|
281
|
+
Array.prototype.slice.call(ev.targetTouches).map(t => preProcessTouch(t, t.identifier))
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
function handleEditorTouchEnd(ev) {
|
|
285
|
+
ev.preventDefault()
|
|
286
|
+
ev.stopPropagation()
|
|
287
|
+
if(dragArea.value && cropArea.value) updateTouches(
|
|
288
|
+
Array.prototype.slice.call(ev.targetTouches).map(t => preProcessTouch(t, t.identifier))
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
function handleEditorTouchCancel(ev) {
|
|
292
|
+
ev.preventDefault()
|
|
293
|
+
ev.stopPropagation()
|
|
294
|
+
if(dragArea.value && cropArea.value) updateTouches(
|
|
295
|
+
Array.prototype.slice.call(ev.targetTouches).map(t => preProcessTouch(t, t.identifier))
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
function handleEditorTouchMove(ev) {
|
|
299
|
+
ev.preventDefault()
|
|
300
|
+
ev.stopPropagation()
|
|
301
|
+
if($refs.editor && $refs.content) updateTouches(
|
|
302
|
+
Array.prototype.slice.call(ev.targetTouches).map(t => preProcessTouch(t, t.identifier))
|
|
303
|
+
)
|
|
304
|
+
}
|
|
305
|
+
function handleEditorWheel(ev) {
|
|
306
|
+
ev.preventDefault()
|
|
307
|
+
ev.stopPropagation()
|
|
308
|
+
const rate = 0.2
|
|
309
|
+
if(ev.deltaY > 0) {
|
|
310
|
+
updatePosition(position.x, position.y, position.scale / (1 + rate))
|
|
311
|
+
} else if(ev.deltaY < 0) {
|
|
312
|
+
updatePosition(position.x, position.y, position.scale * (1 + rate))
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
async function getSourceImageUrl() {
|
|
318
|
+
if(sourceUpload.value) {
|
|
319
|
+
await sourceUpload.value.prepare()
|
|
320
|
+
if(sourceUpload.value.url) {
|
|
321
|
+
return sourceUpload.value.url
|
|
322
|
+
}
|
|
323
|
+
await sourceUpload.value.upload()
|
|
324
|
+
if(sourceUpload.value.url) {
|
|
325
|
+
return sourceUpload.value.url
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if(!sourceImageSize.value) throw new Error("crop not ready")
|
|
329
|
+
return `/api/image/image/${sourceImage.value}`
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function getSourceImageCanvas() {
|
|
333
|
+
if(sourceUpload.value?.canvas) return sourceUpload.value.canvas
|
|
334
|
+
const image = await loadImage(await getSourceImageUrl())
|
|
335
|
+
const canvas = imageToCanvas(image)
|
|
336
|
+
return canvas
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function crop() {
|
|
340
|
+
const ratio = aspectRatio.value / sourceAspectRatio.value
|
|
341
|
+
const xMin = Math.round(((position.x - 1/position.scale) / 2 + 0.5) * sourceImageSize.value.width)
|
|
342
|
+
const xMax = Math.round(((position.x + 1/position.scale) / 2 + 0.5) * sourceImageSize.value.width)
|
|
343
|
+
console.log("X", xMin, xMax)
|
|
344
|
+
const yMin = Math.round(((position.y - 1/position.scale/ratio) / 2 + 0.5) * sourceImageSize.value.height)
|
|
345
|
+
const yMax = Math.round(((position.y + 1/position.scale/ratio) / 2 + 0.5) * sourceImageSize.value.height)
|
|
346
|
+
console.log("Y", yMin, yMax)
|
|
347
|
+
const width = xMax - xMin
|
|
348
|
+
const height = yMax - yMin
|
|
349
|
+
|
|
350
|
+
const crop = {
|
|
351
|
+
x: position.x,
|
|
352
|
+
y: position.y,
|
|
353
|
+
zoom: position.scale,
|
|
354
|
+
orientation: 0,
|
|
355
|
+
originalImage: sourceImage.value || sourceUpload.value.id
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const srcXMin = Math.max(0, xMin)
|
|
359
|
+
const srcYMin = Math.max(0, yMin)
|
|
360
|
+
const srcXMax = Math.min(sourceImageSize.value.width, xMax)
|
|
361
|
+
const srcYMax = Math.max(sourceImageSize.value.height, yMax)
|
|
362
|
+
|
|
363
|
+
const canvas = await getSourceImageCanvas()
|
|
364
|
+
const context = canvas.getContext('2d')
|
|
365
|
+
let imageData = context.getImageData(srcXMin, srcYMin, srcXMax - srcXMin, srcYMax - srcYMin)
|
|
366
|
+
canvas.width = width
|
|
367
|
+
canvas.height = height
|
|
368
|
+
context.clearRect(0, 0, canvas.width, canvas.height)
|
|
369
|
+
context.putImageData(imageData, srcXMin - xMin, srcYMin - yMin)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
return { crop, canvas }
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
defineExpose({
|
|
376
|
+
crop
|
|
377
|
+
})
|
|
378
|
+
</script>
|
|
379
|
+
|
|
380
|
+
<style scoped>
|
|
381
|
+
|
|
382
|
+
</style>
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div v-if="state == 'edit'">
|
|
3
|
+
<ImageCrop v-if="sourceImage || sourceUpload"
|
|
4
|
+
:crop="cropData"
|
|
5
|
+
:aspectRatio="aspectRatio"
|
|
6
|
+
:sourceImage="sourceImage"
|
|
7
|
+
:fill="props.fill"
|
|
8
|
+
:sourceUpload="sourceUpload"
|
|
9
|
+
:type="type"
|
|
10
|
+
v-model:ready="cropReady"
|
|
11
|
+
ref="imageCrop" />
|
|
12
|
+
<div class="flex p-4">
|
|
13
|
+
<div class="flex-grow-1 flex">
|
|
14
|
+
<Button type="button" label="Add Image" icon="pi pi-plus" class="p-button-primary"
|
|
15
|
+
@click="() => state = 'upload' "/>
|
|
16
|
+
</div>
|
|
17
|
+
<div class="flex">
|
|
18
|
+
<Button type="button" label="Save Image" icon="pi pi-save" class="p-button-warning ml-2"
|
|
19
|
+
:disabled="!cropReady" @click="saveImage" />
|
|
20
|
+
<Button type="button" label="Remove Image" icon="pi pi-trash" class="p-button-danger ml-2"
|
|
21
|
+
@click="removeImage"/>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
<div v-else>
|
|
26
|
+
<DropZone class="w-full relative p-6 md:p-8 lg:p-8" :accept="acceptList" @input="handleFile">
|
|
27
|
+
<div class="w-auto border-dashed border-primary-500 flex align-items-center justify-content-center"
|
|
28
|
+
:style="`aspect-ratio: ${aspectRatio}`">
|
|
29
|
+
<p class="text-primary text-xl">Drop image here!</p>
|
|
30
|
+
</div>
|
|
31
|
+
</DropZone>
|
|
32
|
+
<div class="flex p-4">
|
|
33
|
+
<div class="flex-grow-1 flex">
|
|
34
|
+
<FileInput :accept="acceptList" @input="handleFile" class="block">
|
|
35
|
+
<Button type="button" label="Upload Image" icon="pi pi-upload" class="p-button-primary" />
|
|
36
|
+
</FileInput>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</template>
|
|
41
|
+
|
|
42
|
+
<script setup>
|
|
43
|
+
import { computed, ref, watch, inject, getCurrentInstance } from 'vue'
|
|
44
|
+
import { useResizeObserver } from '@vueuse/core'
|
|
45
|
+
import { getElementPositionInWindow, getElementPositionInElement } from "./dom.js"
|
|
46
|
+
|
|
47
|
+
import { path, fetch } from '@live-change/vue3-ssr'
|
|
48
|
+
|
|
49
|
+
import { FileInput, DropZone } from '@live-change/upload-frontend'
|
|
50
|
+
import { uploadImage } from "./imageUploads.js"
|
|
51
|
+
import Button from "primevue/button"
|
|
52
|
+
|
|
53
|
+
import ImageCrop from "./ImageCrop.vue"
|
|
54
|
+
|
|
55
|
+
import { useToast } from 'primevue/usetoast'
|
|
56
|
+
const toast = useToast()
|
|
57
|
+
import { useConfirm } from 'primevue/useconfirm'
|
|
58
|
+
const confirm = useConfirm()
|
|
59
|
+
|
|
60
|
+
const props = defineProps({
|
|
61
|
+
modelValue: { // image
|
|
62
|
+
type: String,
|
|
63
|
+
default: null
|
|
64
|
+
},
|
|
65
|
+
sourceImage: {
|
|
66
|
+
type: String,
|
|
67
|
+
default: null
|
|
68
|
+
},
|
|
69
|
+
uploadedFile: {
|
|
70
|
+
type: (typeof window != "undefined") ? Blob : undefined,
|
|
71
|
+
default: null
|
|
72
|
+
},
|
|
73
|
+
purpose: {
|
|
74
|
+
type: String,
|
|
75
|
+
default: "unknown"
|
|
76
|
+
},
|
|
77
|
+
width: {
|
|
78
|
+
type: Number,
|
|
79
|
+
default: 256
|
|
80
|
+
},
|
|
81
|
+
height: {
|
|
82
|
+
type: Number,
|
|
83
|
+
default: 256
|
|
84
|
+
},
|
|
85
|
+
type: {
|
|
86
|
+
type: String,
|
|
87
|
+
default: 'rect' // or 'circle'
|
|
88
|
+
},
|
|
89
|
+
fill: {
|
|
90
|
+
type: Boolean,
|
|
91
|
+
default: true
|
|
92
|
+
},
|
|
93
|
+
saveButton: {
|
|
94
|
+
type: Boolean
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
const workingZone = inject('workingZone')
|
|
99
|
+
const loadingZone = inject('loadingZone')
|
|
100
|
+
|
|
101
|
+
const aspectRatio = computed(() => props.width/props.height)
|
|
102
|
+
const acceptList = 'image/jpeg, image/png, image/webp, .jpg, .png, .jpeg, .webp'
|
|
103
|
+
|
|
104
|
+
const state = ref(props.modelValue || props.sourceImage ? 'edit' : 'upload')
|
|
105
|
+
|
|
106
|
+
const sourceImage = ref(props.sourceImage)
|
|
107
|
+
const sourceUpload = ref()
|
|
108
|
+
|
|
109
|
+
const upload = ref()
|
|
110
|
+
|
|
111
|
+
const imageType = ref()
|
|
112
|
+
const imageName = ref()
|
|
113
|
+
const cropData = ref({ x: 0, y: 0, zoom: 1, orientation: 0 })
|
|
114
|
+
|
|
115
|
+
const imageCrop = ref()
|
|
116
|
+
const cropReady = ref(false)
|
|
117
|
+
|
|
118
|
+
const appContext = getCurrentInstance().appContext
|
|
119
|
+
|
|
120
|
+
const emit = defineEmits(['update:modelValue', 'close'])
|
|
121
|
+
|
|
122
|
+
async function removeImage(event) {
|
|
123
|
+
confirm.require({
|
|
124
|
+
target: event.currentTarget,
|
|
125
|
+
message: `Do you want to delete image?`,
|
|
126
|
+
icon: 'pi pi-info-circle',
|
|
127
|
+
acceptClass: 'p-button-danger',
|
|
128
|
+
accept: async () => {
|
|
129
|
+
sourceImage.value = null
|
|
130
|
+
state.value = 'upload'
|
|
131
|
+
emit('update:modelValue', null)
|
|
132
|
+
emit('close')
|
|
133
|
+
toast.add({ severity:'info', summary: 'Image Deleted', life: 1500 })
|
|
134
|
+
},
|
|
135
|
+
reject: () => {
|
|
136
|
+
toast.add({ severity:'error', summary: 'Rejected', detail: 'You have rejected', life: 3000 })
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function saveImage() {
|
|
142
|
+
await workingZone.addPromise('crop image and upload', (async () => {
|
|
143
|
+
if(!imageCrop.value) throw new Error("crop not available")
|
|
144
|
+
if(!cropReady.value) throw new Error("crop not ready")
|
|
145
|
+
console.log("IMAGE CROP", imageCrop.value)
|
|
146
|
+
const { crop, canvas } = await imageCrop.value.crop()
|
|
147
|
+
console.log("CROP RESULT", crop, canvas)
|
|
148
|
+
|
|
149
|
+
upload.value = uploadImage(props.purpose, { canvas },
|
|
150
|
+
{ preparedPreview: true, appContext, generateId : true, crop, })
|
|
151
|
+
console.log("START PREPARE!")
|
|
152
|
+
emit('update:modelValue', upload.value.id)
|
|
153
|
+
if(sourceUpload.value) await sourceUpload.value.upload()
|
|
154
|
+
await upload.value.upload()
|
|
155
|
+
emit('close')
|
|
156
|
+
})())
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function handleFile(file) {
|
|
160
|
+
imageName.value = file.name
|
|
161
|
+
imageType.value = file.type
|
|
162
|
+
await workingZone.addPromise("upload source image", (async () => {
|
|
163
|
+
sourceUpload.value = uploadImage(props.purpose, { file },
|
|
164
|
+
{ preparedPreview: true, appContext, generateId : true, saveCanvas: true })
|
|
165
|
+
console.log("START PREPARE!")
|
|
166
|
+
await sourceUpload.value.prepare()
|
|
167
|
+
cropReady.value = false
|
|
168
|
+
sourceImage.value = sourceUpload.value.id
|
|
169
|
+
state.value = 'edit'
|
|
170
|
+
})())
|
|
171
|
+
console.log("START UPLOAD!")
|
|
172
|
+
await sourceUpload.value.upload()
|
|
173
|
+
console.log("SOURCE UPLOADED", sourceUpload.value)
|
|
174
|
+
}
|
|
175
|
+
if(props.uploadedFile) handleFile(props.uploadedFile)
|
|
176
|
+
|
|
177
|
+
if(props.modelValue) { // Existing image
|
|
178
|
+
loadingZone.addPromise("load exsting image metadata", (async () => {
|
|
179
|
+
const imageData = await fetch(
|
|
180
|
+
path().image.image({ image: props.modelValue }).with(
|
|
181
|
+
image => path().image.image({ image: image.crop.originalImage }).bind('originalImage')
|
|
182
|
+
)
|
|
183
|
+
)
|
|
184
|
+
const originalImage = imageData.value.originalImage
|
|
185
|
+
console.log("IM DATA", imageData, originalImage)
|
|
186
|
+
imageName.value = originalImage.name
|
|
187
|
+
const splitName = originalImage.name.split('.')
|
|
188
|
+
const extension = splitName[splitName.length - 1].toLowerCase()
|
|
189
|
+
switch(extension) {
|
|
190
|
+
case 'jpg':
|
|
191
|
+
case 'jpeg':
|
|
192
|
+
imageType.value = 'image/jpeg'
|
|
193
|
+
break
|
|
194
|
+
case 'webp':
|
|
195
|
+
imageType.value = 'image/webp'
|
|
196
|
+
break
|
|
197
|
+
default:
|
|
198
|
+
case 'png':
|
|
199
|
+
imageType.value = 'image/png'
|
|
200
|
+
break
|
|
201
|
+
}
|
|
202
|
+
cropData.value = imageData.value.crop
|
|
203
|
+
sourceImage.value = originalImage.id
|
|
204
|
+
console.log("IMAGE", imageName.value, imageType.value)
|
|
205
|
+
})())
|
|
206
|
+
}
|
|
207
|
+
</script>
|
|
208
|
+
|
|
209
|
+
<style scoped>
|
|
210
|
+
|
|
211
|
+
</style>
|