UNPKG

aico-image-editor

Version:

Combine multiple image into and create single combined image

361 lines (326 loc) 15.7 kB
import Cropper from 'cropperjs'; const elementStore = Alpine.store('elements'); import calculateAspectRatio from 'calculate-aspect-ratio'; Alpine.store('shapeCropperModalStore', { cropperShapeImg: null, cropperData: null, originalShapeFile: null, editingShapeId: null, shouldPushRounded: false, uploadWithoutConfig: false, dataURLtoBlob(dataurl) { var arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1], bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n); while(n--){ u8arr[n] = bstr.charCodeAt(n); } return new Blob([u8arr], {type:mime}); }, 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 editExistingShape(shape) { // originalBackgroundFile was only used for local testing //const originalBackgroundFile = await this.getFileFromUrl(background.originalUrl); this.editingShapeId = shape.id; const cropperInitObj = { fileName: shape.name, //url: shape.cropperData.originalFileDataUrl, url: shape.originalUrl, cropperData: shape.cropperData } this.displaySelectedImage(cropperInitObj); }, async displaySelectedImage(cropperInitObj) { this.destroyShapeCropper() const img = document.createElement('img'); img.id = 'js-img-cropper'; img.src = cropperInitObj.url; img.alt = cropperInitObj.fileName; const imgcrop = elementStore.imgCropEL; imgcrop.innerHTML = ''; imgcrop.appendChild(img); // this represents current shape image element which was created above this.cropperShapeImg = img; this.cropperData = cropperInitObj.cropperData; if(cropperInitObj.originalShapeFile) { this.originalShapeFile = cropperInitObj.originalShapeFile; } this.uploadWithoutConfig = cropperInitObj.uploadWithoutConfig; if(elementStore.cropShapeModalEL.classList.contains('show')) { this.initShapeCropper(img); } else { bootstrap.Modal.getOrCreateInstance(elementStore.cropShapeModalEL).show() } }, initShapeCropper(img) { let self = this; //console.log('cropper is being init now') const input = elementStore.shapeUploadEL; input.value = ''; //this.aspectRatio = '1'; window.dispatchEvent(new CustomEvent('shapes-cleard')) //destroy old cropper before initializing new one new Cropper(img, { crop: function (event) { self.cropBoxWidth = Math.round(event.detail.width); self.cropBoxHeight = Math.round(event.detail.height); }, //aspectRatio: parseFloat(self.aspectRatio), preview: elementStore.cropperShapePreviewEL, autoCropArea: 1, // data:{ //define cropbox size // width: 240, // height: 240, // }, ready: function () { if (self.cropperData) { this.cropper.setData(self.cropperData); if(self.cropperData?.aspectRatio) { self.aspectRatio = 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); } } else { self.resetCropperControls() } // set cropper instance's aspect ratio sane as alpine js object;s aspect ratio property but it should be in number this.aspectRatio = parseFloat(self.aspectRatio); // set dragmode of cropper instance to alpine js dragmode after in by calling this method self.setDragMode(self.dragMode); }, }); // no need to do squaer after init //this.setSquare(); }, 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.cropperShapeImg.cropper; cropperInstance.setDragMode(dragMode) }, zoomMode: 'zoomIn', zoom(zoomValue, zoomMode ) { this.zoomMode = zoomMode; const cropperInstance = this.cropperShapeImg.cropper; cropperInstance.zoom(zoomValue) }, shouldPushRounded: false, setRadius() { this.shouldPushRounded = true; elementStore.cropperShapePreviewEL.classList.add("rounded"); elementStore.cropShapeModalEL.querySelector(".cropper-view-box")?.classList.add("rounded"); elementStore.cropShapeModalEL.querySelector(".cropper-face")?.classList.add("rounded"); }, setSquare() { this.shouldPushRounded = false; elementStore.cropperShapePreviewEL.classList.remove("rounded"); elementStore.cropShapeModalEL.querySelector(".cropper-view-box")?.classList.remove("rounded"); elementStore.cropShapeModalEL.querySelector(".cropper-face")?.classList.remove("rounded"); }, aspectRatio: 'NaN', setAspectRatio(aspectRatio) { this.aspectRatio = aspectRatio; if(aspectRatio) { const cropperInstance = this.cropperShapeImg.cropper; cropperInstance.setAspectRatio(parseFloat(aspectRatio)) } }, clear() { const cropperInstance = this.cropperShapeImg.cropper; cropperInstance.clear() }, destroyShapeCropper() { this.cropperShapeImg?.cropper?.destroy(); }, async crop() { return new Promise((resolve, reject) => { const cropperInstance = this.cropperShapeImg.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.cropperShapeImg.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(); const input = elementStore.shapeUploadEL; input.files = dtFiles; // this is for tempShapes so that it can be shown before upload Array.from(dtFiles).forEach(file => { window.dispatchEvent(new CustomEvent('shape-uploaded', { detail: { name: self.cropperShapeImg.alt, label: self.cropperShapeImg.alt, url: URL.createObjectURL(file), class: self.shouldPushRounded ? 'rounded' : '', type: file.type, } })); }) const cropperData = Object.assign( {originalFileDataUrl: self.cropperShapeImg.src, wasAspectRatioSaved: self.shouldCurrentAspectRatioSaved}, self.cropperShapeImg?.cropper.getData(), ); if(input.files.length) { const uploadObj = { files: input.files, action: `${$store.canvas.apiConfig.apiUrl}/api/v1/product-configurators/${$store.canvas.configuratorId || null}/images`, type: 'shapes[]', imageType: 'SHAPE', originalFile: this.originalShapeFile, cropperData: cropperData } const data = await $store.uploadStore.uploadFilesToServer(uploadObj); data?.data?.images?.forEach((shape) => { window.dispatchEvent(new CustomEvent('shapes-added-from-api', { detail: { shapes: [shape] } })); }); //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(); const input = elementStore.shapeUploadEL; input.files = files; const cropperData = Object.assign({ originalFileDataUrl: self.cropperShapeImg.src, wasAspectRatioSaved: self.shouldCurrentAspectRatioSaved, aspectRatio: self.aspectRatio, shouldPushRounded: self.shouldPushRounded, zoomMode: self.zoomMode, dragMode: self.dragMode } ,self.cropperShapeImg?.cropper.getData()); if(input.files.length) { const editUploadObj = { files: input.files, action: `${$store.canvas.apiConfig.apiUrl}/api/v1/product-configurators/images/${this.editingShapeId}`, 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) { window.dispatchEvent(new CustomEvent('shapes-upated-from-api', { detail: { shapes: [data?.data] } })); } //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 }], })