aico-image-editor
Version:
Combine multiple image into and create single combined image
297 lines (273 loc) • 12 kB
JavaScript
import Cropper from 'cropperjs';
const elementStore = Alpine.store('elements');
Alpine.store('pictureCropperModalStore', {
cropperPictureImg: null,
cropperData: null,
originalPictureFile: null,
editingPictureId: null,
shouldPushRounded: 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 editExistingPicture(picture) {
//const originalBackgroundFile = await this.getFileFromUrl(background.originalUrl);
this.editingPictureId = picture.id;
const cropperInitObj = {
fileName: picture.name,
url: picture.cropperData.originalFileDataUrl,
//url: picture.originalUrl,
cropperData: picture.cropperData
}
this.displaySelectedPicture(cropperInitObj);
},
async displaySelectedPicture(cropperInitObj) {
this.destroyPictureCropper()
const img = document.createElement('img');
img.id = 'js-picture-cropper';
img.src = cropperInitObj.url;
img.alt = cropperInitObj.fileName;
const imgcrop = elementStore.pictureCropEL;
imgcrop.innerHTML = '';
imgcrop.appendChild(img);
// this represents current shape image element which was created above
this.cropperPictureImg = img;
this.cropperData = cropperInitObj.cropperData;
if(cropperInitObj.originalPictureFile) {
this.originalPictureFile = cropperInitObj.originalPictureFile;
}
if(elementStore.cropPictureModalEL.classList.contains('show')) {
this.initPictureCropper(img);
} else {
bootstrap.Modal.getOrCreateInstance(elementStore.cropPictureModalEL).show()
}
},
initPictureCropper(img) {
let self = this;
//console.log('cropper is being init now')
const input = elementStore.pictureUploadEL;
input.value = '';
//this.aspectRatio = '1';
window.dispatchEvent(new CustomEvent('pictures-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.cropperPicturePreviewEL,
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.dragMode = 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() {
this.setAspectRatio('NaN');
this.setSquare();
this.setDragMode('move');
this.zoomMode = 'zoomIn'
},
dragMode: 'move',
setDragMode(dragMode) {
this.dragMode = dragMode;
const cropperInstance = this.cropperPictureImg.cropper;
cropperInstance.setDragMode(dragMode)
},
zoomMode: 'zoomIn',
zoom(zoomValue, zoomMode ) {
this.zoomMode = zoomMode;
const cropperInstance = this.cropperPictureImg.cropper;
cropperInstance.zoom(zoomValue)
},
shouldPushRounded: false,
setRadius() {
this.shouldPushRounded = true;
elementStore.cropperPicturePreviewEL.classList.add("rounded");
elementStore.cropPictureModalEL.querySelector(".cropper-view-box")?.classList.add("rounded");
elementStore.cropPictureModalEL.querySelector(".cropper-face")?.classList.add("rounded");
},
setSquare() {
this.shouldPushRounded = false;
elementStore.cropperPicturePreviewEL.classList.remove("rounded");
elementStore.cropPictureModalEL.querySelector(".cropper-view-box")?.classList.remove("rounded");
elementStore.cropPictureModalEL.querySelector(".cropper-face")?.classList.remove("rounded");
},
aspectRatio: 'NaN',
setAspectRatio(aspectRatio) {
this.aspectRatio = aspectRatio;
if(aspectRatio) {
const cropperInstance = this.cropperPictureImg.cropper;
cropperInstance.setAspectRatio(aspectRatio)
}
},
clear() {
const cropperInstance = this.cropperPictureImg.cropper;
cropperInstance.clear()
},
destroyPictureCropper() {
this.cropperPictureImg?.cropper?.destroy();
},
async crop() {
return new Promise((resolve, reject) => {
const cropperInstance = this.cropperPictureImg.cropper;
const input = elementStore.pictureUploadEL;
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.cropperPictureImg.alt, { type: blob.type }));
input.files = dt.files;
window.dispatchEvent(new CustomEvent('picture-uploaded', {
detail: {
name: self.cropperPictureImg.alt,
label: self.cropperPictureImg.alt,
url: URL.createObjectURL(blob),
class: self.shouldPushRounded ? 'rounded' : '',
type: blob.type,
}
}));
resolve(dt.files);
} else {
reject("Failed to generate blob");
}
});
});
},
async processUpload($store) {
let self = this;
await this.crop();
const input = elementStore.pictureUploadEL;
const cropperData = Object.assign(
{originalFileDataUrl: self.cropperPictureImg.src, wasAspectRatioSaved: self.shouldCurrentAspectRatioSaved} ,
self.cropperPictureImg?.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: 'mainPictures[]',
imageType: 'MAIN_IMAGE',
originalFile: this.originalPictureFile,
cropperData: cropperData
}
const data = await $store.uploadStore.uploadFilesToServer(uploadObj);
data?.data?.images?.forEach((mainPicture) => {
window.dispatchEvent(new CustomEvent('main-pictures-added-from-api', {
detail: {
mainPictures: [mainPicture]
}
}));
});
//productBlockVisibleMobile = true;
}
},
async processUpdate($store) {
let self = this;
const files = await this.crop();
const input = elementStore.pictureUploadEL;
input.files = files;
const cropperData = Object.assign({
originalFileDataUrl: self.cropperPictureImg.src,
wasAspectRatioSaved: self.shouldCurrentAspectRatioSaved,
aspectRatio: self.aspectRatio,
shouldPushRounded: self.shouldPushRounded,
zoomMode: self.zoomMode,
dragMode: self.dragMode
} ,self.cropperPictureImg?.cropper.getData());
if(input.files.length) {
const editUploadObj = {
files: input.files,
action: `${$store.canvas.apiConfig.apiUrl}/api/v1/product-configurators/images/${this.editingPictureId}`,
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('main-pictures-updated-from-api', {
detail: {
mainPictures: [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,
})