aico-image-editor
Version:
Combine multiple image into and create single combined image
384 lines (324 loc) • 14.3 kB
JavaScript
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
}],
})