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