Skip to content
This repository was archived by the owner on Jan 23, 2025. It is now read-only.

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: update crop modal (v1)
Browse files Browse the repository at this point in the history
spaenleh committed Jan 23, 2024

Verified

This commit was signed with the committer’s verified signature.
spaenleh Basile Spaenlehauer
1 parent 5492143 commit db8206e
Showing 4 changed files with 135 additions and 77 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -18,7 +18,7 @@
"@emotion/react": "11.11.1",
"@emotion/styled": "11.11.0",
"@graasp/chatbox": "3.0.1",
"@graasp/query-client": "2.3.0",
"@graasp/query-client": "2.3.2",
"@graasp/sdk": "3.4.1",
"@graasp/translations": "1.22.1",
"@graasp/ui": "4.3.1",
@@ -55,7 +55,7 @@
"react-dom": "18.2.0",
"react-ga4": "2.1.0",
"react-i18next": "13.5.0",
"react-image-crop": "9.1.1",
"react-image-crop": "11.0.5",
"react-qr-code": "2.0.12",
"react-query": "3.39.3",
"react-quill": "2.0.0",
168 changes: 108 additions & 60 deletions src/components/common/CropModal.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { useRef, useState } from 'react';
import ReactCrop, { Crop, PixelCrop } from 'react-image-crop';
import { ReactEventHandler, useRef, useState } from 'react';
import ReactCrop, {
Crop,
PixelCrop,
centerCrop,
makeAspectCrop,
} from 'react-image-crop';
import 'react-image-crop/dist/ReactCrop.css';

import Dialog from '@mui/material/Dialog';
@@ -12,12 +17,30 @@ import { Button } from '@graasp/ui';

import { THUMBNAIL_ASPECT } from '../../config/constants';
import { useBuilderTranslation } from '../../config/i18n';
import notifier from '../../config/notifier';
import { CROP_MODAL_CONFIRM_BUTTON_CLASSNAME } from '../../config/selectors';
import { BUILDER } from '../../langs/constants';
import { getCroppedImg } from '../../utils/image';
import CancelButton from './CancelButton';

function centerAspectCrop(
mediaWidth: number,
mediaHeight: number,
aspect: number,
) {
return centerCrop(
makeAspectCrop(
{
unit: '%',
width: 90,
},
aspect,
mediaWidth,
mediaHeight,
),
mediaWidth,
mediaHeight,
);
}

export type CropProps = {
open: boolean;
onClose: () => void;
@@ -31,68 +54,83 @@ const CropModal = ({
onClose,
src,
}: CropProps): JSX.Element => {
const [crop, setCrop] = useState<PixelCrop>();
const imageRef = useRef<HTMLImageElement>();
const [crop, setCrop] = useState<Crop>();
const [completedCrop, setCompletedCrop] = useState<PixelCrop>();
const imageRef = useRef<HTMLImageElement | null>(null);
const { t } = useBuilderTranslation();

const makeClientCrop = async (newCrop: PixelCrop) => {
if (imageRef.current && newCrop.width && newCrop.height) {
const croppedImage = await getCroppedImg(imageRef.current, newCrop);
return croppedImage;
const handleOnConfirm = async () => {
const image = imageRef.current;
if (!image || !completedCrop) {
throw new Error('Crop canvas does not exist');
}
return null;
};

const handleOnConfirm = async () => {
if (!crop) {
notifier({
type: 'crop',
payload: { error: new Error('crop is undefined') },
});
} else {
const final = await makeClientCrop(crop);
onConfirm(final);
const offscreen = new OffscreenCanvas(
completedCrop.width,
completedCrop.height,
);
const ctx = offscreen.getContext('2d');
if (!ctx) {
throw new Error('No 2d context');
}

const scaleX = image.naturalWidth / image.width;
const scaleY = image.naturalHeight / image.height;
// devicePixelRatio slightly increases sharpness on retina devices
// at the expense of slightly slower render times and needing to
// size the image back down if you want to download/upload and be
// true to the images natural size.
const pixelRatio = window.devicePixelRatio;
// const pixelRatio = 1

offscreen.width = Math.floor(completedCrop.width * scaleX * pixelRatio);
offscreen.height = Math.floor(completedCrop.height * scaleY * pixelRatio);

ctx.scale(pixelRatio, pixelRatio);
ctx.imageSmoothingQuality = 'high';

const cropX = completedCrop.x * scaleX;
const cropY = completedCrop.y * scaleY;

const centerX = image.naturalWidth / 2;
const centerY = image.naturalHeight / 2;

ctx.save();

// 5) Move the crop origin to the canvas origin (0,0)
ctx.translate(-cropX, -cropY);
// 4) Move the origin to the center of the original position
ctx.translate(centerX, centerY);
// 1) Move the center of the image to the origin (0,0)
ctx.translate(-centerX, -centerY);
ctx.drawImage(
image,
0,
0,
image.naturalWidth,
image.naturalHeight,
0,
0,
image.naturalWidth,
image.naturalHeight,
);

// You might want { type: "image/jpeg", quality: <0 to 1> } to
// reduce image size
const blob = await offscreen.convertToBlob({
type: 'image/png',
});
onConfirm(blob);
};

// If you setState the crop in here you should return false.
const onImageLoaded = (img: HTMLImageElement) => {
const onImageLoaded: ReactEventHandler<HTMLImageElement> = (event) => {
if (!imageRef.current) {
imageRef.current = img;
imageRef.current = event.currentTarget;
}
const { width: imgWidth, height: imgHeight } = img;

const aspect = THUMBNAIL_ASPECT;
const outputImageAspectRatio = aspect;
const inputImageAspectRatio = imgWidth / imgHeight;

let width = imgWidth;
let height = imgHeight;
// if it's bigger than our target aspect ratio
if (inputImageAspectRatio > outputImageAspectRatio) {
width = imgHeight * outputImageAspectRatio;
} else if (inputImageAspectRatio < outputImageAspectRatio) {
height = imgWidth / outputImageAspectRatio;
}

const y = Math.floor((imgHeight - height) / 2);
const x = Math.floor((imgWidth - width) / 2);

const newCrop = {
width,
height,
x,
y,
aspect,
unit: 'px',
} as PixelCrop;
setCrop(newCrop);

return false; // Return false if you set crop state in here.
};

const onCropChange = (newCrop: Crop, _percentageCrop: Crop) => {
setCrop(newCrop as PixelCrop);
const { width: imgWidth, height: imgHeight } = event.currentTarget;
setCrop(centerAspectCrop(imgWidth, imgHeight, THUMBNAIL_ASPECT));
};

const label = 'crop-modal-title';
@@ -105,12 +143,22 @@ const CropModal = ({
{t(BUILDER.CROP_IMAGE_MODAL_CONTENT_TEXT)}
</DialogContentText>
<ReactCrop
src={src}
crop={crop ?? {}}
crop={crop}
onChange={(_, percentageCrop) => setCrop(percentageCrop)}
onComplete={(c) => setCompletedCrop(c)}
aspect={THUMBNAIL_ASPECT}
// circularCrop
ruleOfThirds
onImageLoaded={onImageLoaded}
onChange={onCropChange}
/>
>
<img
ref={imageRef}
alt="Crop me"
width="100%"
height="100%"
src={src}
onLoad={onImageLoaded}
/>
</ReactCrop>
</DialogContent>
<DialogActions>
<CancelButton onClick={onClose} />
4 changes: 2 additions & 2 deletions src/utils/image.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { PixelCrop } from 'react-image-crop';
import { Crop } from 'react-image-crop';

import { THUMBNAIL_EXTENSION } from '../config/constants';

// eslint-disable-next-line import/prefer-default-export
export const getCroppedImg = (
image: HTMLImageElement,
crop: PixelCrop,
crop: Crop,
extension = THUMBNAIL_EXTENSION,
): Promise<Blob> => {
const canvas = document.createElement('canvas');
36 changes: 23 additions & 13 deletions yarn.lock
Original file line number Diff line number Diff line change
@@ -1233,11 +1233,11 @@ __metadata:
languageName: node
linkType: hard

"@graasp/query-client@npm:2.3.0":
version: 2.3.0
resolution: "@graasp/query-client@npm:2.3.0"
"@graasp/query-client@npm:2.3.2":
version: 2.3.2
resolution: "@graasp/query-client@npm:2.3.2"
dependencies:
"@graasp/sdk": "npm:3.4.1"
"@graasp/sdk": "npm:3.5.0"
"@graasp/translations": "npm:1.22.1"
axios: "npm:0.27.2"
crypto-js: "npm:4.2.0"
@@ -1247,7 +1247,7 @@ __metadata:
uuid: "npm:9.0.1"
peerDependencies:
react: ^17.0.0 || ^18.0.0
checksum: 7cbf30cc71c27e19b50d896899b2bed3de924c4d7b0d09346198ee07c0d640261908fdb718cce11b00888f2333b581e175ca2d31bac766163b27ef069a29abfa
checksum: a998abfc7a826ce789c32e8d7112811a2e6f0e22a41ff476d5939bb35a6eac22b6167e32c8c47b5100fe889b26612f71cf83bb439be18f9f13a4e8e3e5b8644c
languageName: node
linkType: hard

@@ -1276,6 +1276,18 @@ __metadata:
languageName: node
linkType: hard

"@graasp/sdk@npm:3.5.0":
version: 3.5.0
resolution: "@graasp/sdk@npm:3.5.0"
dependencies:
date-fns: "npm:3.2.0"
js-cookie: "npm:3.0.5"
uuid: "npm:9.0.1"
validator: "npm:13.11.0"
checksum: dfbb9977904336214e411c93a33084fca8e07cb73145f655a070dfdb81a772d6a0412849ee07c8ee2e74497d59f960a8ef1989062a4e41d78ad5d22b24e8a8db
languageName: node
linkType: hard

"@graasp/translations@npm:1.22.1":
version: 1.22.1
resolution: "@graasp/translations@npm:1.22.1"
@@ -6310,7 +6322,7 @@ __metadata:
"@emotion/react": "npm:11.11.1"
"@emotion/styled": "npm:11.11.0"
"@graasp/chatbox": "npm:3.0.1"
"@graasp/query-client": "npm:2.3.0"
"@graasp/query-client": "npm:2.3.2"
"@graasp/sdk": "npm:3.4.1"
"@graasp/translations": "npm:1.22.1"
"@graasp/ui": "npm:4.3.1"
@@ -6383,7 +6395,7 @@ __metadata:
react-dom: "npm:18.2.0"
react-ga4: "npm:2.1.0"
react-i18next: "npm:13.5.0"
react-image-crop: "npm:9.1.1"
react-image-crop: "npm:11.0.5"
react-qr-code: "npm:2.0.12"
react-query: "npm:3.39.3"
react-quill: "npm:2.0.0"
@@ -9833,14 +9845,12 @@ __metadata:
languageName: node
linkType: hard

"react-image-crop@npm:9.1.1":
version: 9.1.1
resolution: "react-image-crop@npm:9.1.1"
dependencies:
clsx: "npm:^1.1.1"
"react-image-crop@npm:11.0.5":
version: 11.0.5
resolution: "react-image-crop@npm:11.0.5"
peerDependencies:
react: ">=16.13.1"
checksum: 806d809f965f111e3e792d99d316b2786f9d90113a6abcfc5b20f6156d669dbd194970eece39a6a490b42330e1b05f25bc4215ece7d41187d57e9eecaecebd99
checksum: 0af15aa70758949797c992b0c1d21a398185e36a7e829d241cfc4bcfe679dd22bd4d0a0a74db78b079c3760315067ff4156dd2d0ecffa6b62a8e87f067e0f271
languageName: node
linkType: hard

0 comments on commit db8206e

Please sign in to comment.