UNPKG

aico-image-editor

Version:

Combine multiple image into and create single combined image

384 lines (324 loc) 14.3 kB
import Cropper from 'cropperjs'; const elementStore = Alpine.store('elements'); import calculateAspectRatio from 'calculate-aspect-ratio'; Alpine.store('cropperStore', { cropImage: null, cropperData: null, originalImageFile: null, editingImageId: null, shouldPushRounded: false, uploadWithoutConfig: false, fileType: '', currentEl: null, async getFileFromUrl(url, name, defaultType = 'image/jpeg') { const response = await fetch(url); const data = await response.blob(); return new File([data], name, { type: data.type || defaultType, }); }, async editExistingImage(image, fileType, el) { //const originalImageFile = await this.getFileFromUrl(background.originalUrl); this.editingImageId = image.id; const cropperInitObj = { fileName: image.name, url: image.originalUrl || image.cropperData.originalFileDataUrl, cropperData: image.cropperData, fileType: fileType, el: el } this.prepareCropperImage(cropperInitObj); }, async prepareCropperImage(cropperInitObj) { // common event to indicate that cropper is going to init now. window.dispatchEvent(new CustomEvent(`cropper-init`)) //destroy old cropper before initializing new one this.destroyCropper() const img = document.createElement('img'); img.id = 'js-cropper'; img.src = cropperInitObj.url; img.alt = cropperInitObj.fileName; const imgcrop = elementStore.imgCropEL; imgcrop.innerHTML = ''; imgcrop.appendChild(img); // this represents current background image element which was created from file above this.cropImage = img; this.cropperData = cropperInitObj.cropperData; if(cropperInitObj.originalImageFile) { this.originalImageFile = cropperInitObj.originalImageFile; } this.fileType= cropperInitObj.fileType; this.currentEl = cropperInitObj.el; this.uploadWithoutConfig = cropperInitObj.uploadWithoutConfig; this.initCropper(img) }, initCropper(img) { let self = this; this.currentEl.value = ''; // if (this.fileType === 'backgrounds') { // this.aspectRatio = '1'; // } window.dispatchEvent(new CustomEvent(`${this.fileType}-cleard`)) new Cropper(img, { crop: function (event) { self.cropBoxWidth = Math.round(event.detail.width); self.cropBoxHeight = Math.round(event.detail.height); }, //aspectRatio: 1, autoCropArea: 1, preview: elementStore.cropperPreviewEL, ready: function () { if(self.cropperData) { if(self.cropperData.aspectRatio) { self.setAspectRatio(self.cropperData.aspectRatio); } if(self.cropperData.shouldPushRounded) { self.setRadius(); } else { self.setSquare(); } if(self.cropperData.zoomMode) { self.zoomMode = self.cropperData.zoomMode; } if(self.cropperData.dragMode) { self.setDragMode(self.cropperData.dragMode); } this.cropper.setData(self.cropperData); } else { self.resetCropperControls() } }, }); }, cropBoxWidth: 0, cropBoxHeight: 0, resetCropperControls() { // except zoomMode all other methods sync the data in alpine and also // associated settings in croppper instance this.setAspectRatio('NaN'); this.setSquare(); this.setDragMode('move'); this.zoomMode = 'zoomIn'; }, dragMode: 'move', setDragMode(dragMode) { this.dragMode = dragMode; const cropperInstance = this.cropImage.cropper; cropperInstance.setDragMode(dragMode) }, zoomMode: 'zoomIn', zoom(zoomValue, zoomMode ) { this.zoomMode = zoomMode; const cropperInstance = this.cropImage.cropper; cropperInstance.zoom(zoomValue) }, setRadius() { this.shouldPushRounded = true; elementStore.cropperPreviewEL.classList.add("rounded"); elementStore.imgCropEL.querySelector(".cropper-view-box")?.classList.add("rounded"); elementStore.imgCropEL.querySelector(".cropper-face")?.classList.add("rounded"); }, setSquare() { this.shouldPushRounded = false; elementStore.cropperPreviewEL.classList.remove("rounded"); elementStore.imgCropEL.querySelector(".cropper-view-box")?.classList.remove("rounded"); elementStore.imgCropEL.querySelector(".cropper-face")?.classList.remove("rounded"); }, aspectRatio: 'NaN', setAspectRatio(aspectRatio) { this.aspectRatio = aspectRatio; if(aspectRatio) { const cropperInstance = this.cropImage.cropper; cropperInstance.setAspectRatio(parseFloat(aspectRatio)) } }, clear() { const cropperInstance = this.cropImage.cropper; cropperInstance.clear() }, destroyCropper() { this.cropImage?.cropper?.destroy(); }, async crop() { return new Promise((resolve, reject) => { const cropperInstance = this.cropImage.cropper; let self = this; const croppedCanvas = cropperInstance.getCroppedCanvas(); let finalCanvas = croppedCanvas; if (self.shouldPushRounded) { finalCanvas = self.getRoundedCanvas(croppedCanvas); } finalCanvas.toBlob(blob => { if (blob) { const dt = new DataTransfer(); dt.items.add(new File([blob], self.cropImage.alt, { type: blob.type })); resolve(dt.files); } else { reject("Failed to generate blob"); } }); }); }, async processUpload($store) { let self = this; try { const dtFiles = await this.crop(); this.currentEl.files = dtFiles; // this is needed for viewing tempBackgrounds/tempShapes/tempMainPictures Array.from(dtFiles).forEach(file => { window.dispatchEvent(new CustomEvent(`${this.fileType}-uploaded`, { detail: { name: self.cropImage.alt, label: self.cropImage.alt, url: URL.createObjectURL(file), class: self.shouldPushRounded ? 'rounded' : '', type: file.type, } })); }) if(this.currentEl.files.length) { const cropperData = Object.assign({ originalFileDataUrl: self.cropImage.src, wasAspectRatioSaved: self.shouldCurrentAspectRatioSaved, aspectRatio: self.aspectRatio, shouldPushRounded: self.shouldPushRounded, zoomMode: self.zoomMode, dragMode: self.dragMode } , self.cropImage?.cropper.getData() ); // for backend purpose we need imageType let imageType; if(this.fileType === 'backgrounds') { imageType = 'BACKGROUND' } else if (this.fileType === 'shapes') { imageType = 'SHAPE' } else if (this.fileType === 'mainPictures') { imageType = 'MAIN_IMAGE'; } const uploadObj = { files: this.currentEl.files, action: `${$store.canvas.apiConfig.apiUrl}/api/v1/product-configurators/${$store.canvas.configuratorId || null}/images`, type: `${this.fileType}[]`, imageType: imageType, originalFile: this.originalImageFile, cropperData: cropperData } const data = await $store.uploadStore.uploadFilesToServer(uploadObj); data?.data?.images?.forEach((image) => { const detailObj = { detail: {} } detailObj.detail[this.fileType] = [image] window.dispatchEvent(new CustomEvent(`${this.fileType}-added-from-api`, detailObj)); }); //productBlockVisibleMobile = true; } } catch (error) { console.log(error); } }, async processUploadWithoutConfigurator($store) { // this method was made later only for sending image as a file and returning back it's url from server let self = this; try { const dtFiles = await this.crop(); const uploadObj = { files: dtFiles, action: `${$store.canvas.apiConfig.apiUrl}/api/v1/product-configurators/save-image-on-server-and-return-file-url`, } const data = await $store.uploadStore.uploadImageWithoutConfigurator(uploadObj); // after uploaded and got url from server, it's time to replace with what we got after cropping and uploading data?.data?.url && $store.canvas.replaceActiveObjectUrl(data?.data?.url); } catch(error) { console.log(error); } }, async processUpdate($store) { let self = this; const files = await this.crop(); this.currentEl.files = files; const cropperData = Object.assign({ originalFileDataUrl: self.cropImage.src, wasAspectRatioSaved: self.shouldCurrentAspectRatioSaved, aspectRatio: self.aspectRatio, shouldPushRounded: self.shouldPushRounded, zoomMode: self.zoomMode, dragMode: self.dragMode }, self.cropImage?.cropper.getData() ); if(this.currentEl.files.length) { const editUploadObj = { files: this.currentEl.files, action: `${$store.canvas.apiConfig.apiUrl}/api/v1/product-configurators/images/${self.editingImageId}`, type: 'image', cropperData: cropperData } const data = await $store.uploadStore.updateImageInServer(editUploadObj); //here for update case background is directly returned as data.data and no further background property under that if(data?.data) { const detailObj = { detail: {} } detailObj.detail[this.fileType] = [data?.data] window.dispatchEvent(new CustomEvent(`${this.fileType}-updated-from-api`, detailObj)); } //productBlockVisibleMobile = true; } }, getRoundedCanvas(sourceCanvas,aspectRatio) { var canvas = document.createElement('canvas'); var context = canvas.getContext('2d'); var width = sourceCanvas.width; var height = sourceCanvas.height; if (width > 0 && height > 0) { if (width / height > aspectRatio) { var newWidth = height * aspectRatio; var xOffset = (width - newWidth) / 2; canvas.width = newWidth; canvas.height = height; context.drawImage(sourceCanvas, xOffset, 0, newWidth, height, 0, 0, newWidth, height); } else { canvas.width = width; canvas.height = height; context.drawImage(sourceCanvas, 0, 0, width, height, 0, 0, width, height); } context.imageSmoothingEnabled = true; context.globalCompositeOperation = 'destination-in'; var centerX = canvas.width / 2; var centerY = canvas.height / 2; var radiusX = canvas.width / 2; var radiusY = canvas.height / 2; context.beginPath(); context.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, 2 * Math.PI); context.fill(); } return canvas; }, shouldCurrentAspectRatioSaved: false, populateAspectRatio(shapeOrPictureObj) { if(shapeOrPictureObj.cropperData && shapeOrPictureObj.cropperData.wasAspectRatioSaved) { const width = shapeOrPictureObj.cropperData.width; const height = shapeOrPictureObj.cropperData.height; if (width > 0 && height > 0) { const aspectRatioLabel = calculateAspectRatio(parseInt(width), parseInt(height)) const aspectRatioValue = (width / height).toFixed(2); // prevent repushing the same ratio if(!(this.savedAspectRatios.find(savedAspectRatio => savedAspectRatio.value === aspectRatioValue))) { this.savedAspectRatios.push({ label: aspectRatioLabel, value: aspectRatioValue }) } } } }, savedAspectRatios: [{ label: '16:10', value: 1.6 }, { label: '5:4', value: 1.25 }], })