- Backend: - Implemented robust Veo video generation including 'lastFrame' and 'referenceImages' support - Fixed video URI extraction with recursive search for API response stability - Implemented direct HTTP video download to resolve SDK method missing errors - Frontend (Video Generator): - Updated validation to allow Text-to-Video for Veo without requiring a first frame - Fixed job state clearing to prevent UI from showing previous completion status - Frontend (My Files & Library): - Moved batch actions toolbar to bottom-left to prevent blocking pagination - Added 'Deselect All' button to batch actions toolbar - Added file type indicators to asset cards - Components: - Added 'Clear Finished' button to Active Jobs tracker - Updated Asset Library modal toolbar positioning
93 lines
2.9 KiB
TypeScript
93 lines
2.9 KiB
TypeScript
export const createImage = (url: string): Promise<HTMLImageElement> =>
|
|
new Promise((resolve, reject) => {
|
|
const image = new Image();
|
|
image.addEventListener('load', () => resolve(image));
|
|
image.addEventListener('error', (error) => reject(error));
|
|
image.setAttribute('crossOrigin', 'anonymous'); // needed to avoid cross-origin issues on CodeSandbox
|
|
image.src = url;
|
|
});
|
|
|
|
export function getRadianAngle(degreeValue: number) {
|
|
return (degreeValue * Math.PI) / 180;
|
|
}
|
|
|
|
/**
|
|
* Returns the new bounding area of a rotated rectangle.
|
|
*/
|
|
export function rotateSize(width: number, height: number, rotation: number) {
|
|
const rotRad = getRadianAngle(rotation);
|
|
|
|
return {
|
|
width:
|
|
Math.abs(Math.cos(rotRad) * width) + Math.abs(Math.sin(rotRad) * height),
|
|
height:
|
|
Math.abs(Math.sin(rotRad) * width) + Math.abs(Math.cos(rotRad) * height),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* This function was adapted from the one in the Readme of https://github.com/DominicTobias/react-image-crop
|
|
*/
|
|
export default async function getCroppedImg(
|
|
imageSrc: string,
|
|
pixelCrop: { x: number; y: number; width: number; height: number },
|
|
rotation = 0,
|
|
flip = { horizontal: false, vertical: false }
|
|
): Promise<Blob> {
|
|
const image = await createImage(imageSrc);
|
|
const canvas = document.createElement('canvas');
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
if (!ctx) {
|
|
return Promise.reject(new Error('No 2d context'));
|
|
}
|
|
|
|
const rotRad = getRadianAngle(rotation);
|
|
|
|
// calculate bounding box of the rotated image
|
|
const { width: bBoxWidth, height: bBoxHeight } = rotateSize(
|
|
image.width,
|
|
image.height,
|
|
rotation
|
|
);
|
|
|
|
// set canvas size to match the bounding box
|
|
canvas.width = bBoxWidth;
|
|
canvas.height = bBoxHeight;
|
|
|
|
// translate canvas context to a central location to allow rotating and flipping around the center
|
|
ctx.translate(bBoxWidth / 2, bBoxHeight / 2);
|
|
ctx.rotate(rotRad);
|
|
ctx.scale(flip.horizontal ? -1 : 1, flip.vertical ? -1 : 1);
|
|
ctx.translate(-image.width / 2, -image.height / 2);
|
|
|
|
// draw rotated image
|
|
ctx.drawImage(image, 0, 0);
|
|
|
|
const data = ctx.getImageData(
|
|
pixelCrop.x,
|
|
pixelCrop.y,
|
|
pixelCrop.width,
|
|
pixelCrop.height
|
|
);
|
|
|
|
// set canvas width to final desired crop size - this will clear existing context
|
|
canvas.width = pixelCrop.width;
|
|
canvas.height = pixelCrop.height;
|
|
|
|
// paste generated rotate image at the top left corner
|
|
ctx.putImageData(data, 0, 0);
|
|
|
|
// As Blob
|
|
return new Promise((resolve, reject) => {
|
|
canvas.toBlob((file) => {
|
|
if (file) {
|
|
// @ts-ignore
|
|
file.name = 'cropped.jpg';
|
|
resolve(file);
|
|
} else {
|
|
reject(new Error('Canvas is empty'));
|
|
}
|
|
}, 'image/jpeg');
|
|
});
|
|
}
|