8
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【React】React Image Cropでトリミング画像の登録機能を実装する

Posted at

はじめに

プロフィール入力フォームをReactで実装するにあたってreact-image-cropを利用しました。
「NoImageの写真をクリックすることで登録する写真を選択できるようにする」という部分に少々詰まったので、あわせて記事にまとめることにしました。

実装

どのような実装を行ったのか、登録ステップごとに記載します。

写真選択


トリミング・登録

写真選択

NoImageの三列表示についてはReact Bootstrapを使用しました。
Reactで置き換える前の画面にBootstrapを使っていたので、デザインを近づけるために今回も使用しました。

InputProfilePicture.tsx
export const InputProfilePicture = () => {
  const types = [1, 2, 3];

  return (
    <Container>
      <Row>
        {types.map((type) => {
          return (
            <Col xs={4} md={4} key={type}>
              <SelectPicture type={type} />
            </Col>
          );
        })}
      </Row>
    </Container>
  );
};

NoImageをクリックして写真を選択できるようにするために、img(Image)要素とinput要素をlabel要素でラップします。
この際、inputが表示されないように、styleにdisplay: 'none'を設定します。

また、マウスホバーで写真がホップして表示されるように、styleにopacity boxShadow transformを追加します。
ホバーの有無の状態を管理するためにconst [isHover, setIsHover] = useState(false)を作成し、onMouseEnter()onMouseLeave()内で状態を切り替えるようにします。

SelectPicture.tsx
export const SelectPicture = ({ type }: { type: number }) => {
  const [isHover, setIsHover] = useState(false);

  return (
    <>
      <label>
        <Image
          src={result}
          roundedCircle
          style={{
            maxWidth: 350,
            border: 'solid 1px',
            cursor: 'pointer',
            opacity: isHover ? 0.8 : 1,
            boxShadow: isHover ? '1px 1px 5px 1px #000' : 'none',
            transform: isHover ? 'translateY(-2px)' : 'none',
          }}
          onMouseEnter={() => setIsHover(true)}
          onMouseLeave={() => setIsHover(false)}
        />
        <input
          type="file"
          accept="image/jpeg,image/png"
          onChange={handleFileChange}
          style={{ display: 'none' }}
        />
      </label>
  );
};

トリミング・登録

写真のトリミングと登録についてはreact-image-cropというライブラリを使用しました。

写真を選択するとモーダルTrimmingModalが出現し、ReactCropで写真のトリミングができるようになります。

SelectPicture.tsx
export const SelectPicture = ({ type }: { type: number }) => {
  const [show, setShow] = useState(false);
  const [src, setSrc] = useState<string>('');
  const [result, setResult] = useState<any>(NoImage);
  const [imageInput, setImageInput] = useState<any>({ type });
  const [isHover, setIsHover] = useState(false);

  const [crop, setCrop, setImage] = useImageCrop(
    type,
    imageInput,
    setImageInput,
    setResult
  );
  const [saveCroppedImg] = useImageSave(setShow, imageInput);

  const handleFileChange = (e: any) => {
    setSrc(URL.createObjectURL(e.target.files[0]));
    setShow(true);
  };

  return (
    <>
      <label>
        <Image
          src={result}
          roundedCircle
          style={{
            maxWidth: 350,
            border: 'solid 1px',
            cursor: 'pointer',
            opacity: isHover ? 0.8 : 1,
            boxShadow: isHover ? '1px 1px 5px 1px #000' : 'none',
            transform: isHover ? 'translateY(-2px)' : 'none',
          }}
          onMouseEnter={() => setIsHover(true)}
          onMouseLeave={() => setIsHover(false)}
        />
        <input
          type="file"
          accept="image/jpeg,image/png"
          onChange={handleFileChange}
          style={{ display: 'none' }}
        />
      </label>

      <TrimmingModal
        show={show}
        setShow={setShow}
        saveCroppedImg={saveCroppedImg}
      >
        <ReactCrop
          src={src}
          onImageLoaded={setImage}
          crop={crop}
          onChange={setCrop}
        />
      </TrimmingModal>
    </>
  );
};
TrimmingModal.tsx
export const TrimmingModal = ({
  show,
  setShow,
  saveCroppedImg,
  children,
}: any) => {
  const handleClose = () => setShow(false);

  return (
    <Modal
      show={show}
      onHide={handleClose}
      backdrop="static"
      keyboard={false}
      size="lg"
      aria-labelledby="contained-modal-title-vcenter"
      centered
    >
      <Modal.Header closeButton>
        <Modal.Title>トリミング</Modal.Title>
      </Modal.Header>
      <Modal.Body className="text-center">{children}</Modal.Body>
      <Modal.Footer>
        <Button variant="secondary" onClick={handleClose}>
          キャンセル
        </Button>
        <Button variant="primary" onClick={saveCroppedImg}>
          登録
        </Button>
      </Modal.Footer>
    </Modal>
  );
};

トリミングおよび登録のロジック部分については、カスタムフックuseImageCropuseImageSaveに切りわけました。

トリミングする写真のアスペクト比aspect、幅初期値width、高さ初期値heightについてはcropuseStateの初期値で設定します。
トリミングサイズや場所が変更されるごとにresultimageInputの値が更新されます。

userImageCrop.ts
export const useImageCrop = (
  type: number,
  imageInput: any,
  setImageInput: Dispatch<SetStateAction<any>>,
  setResult: Dispatch<SetStateAction<any>>
) => {
  const [crop, setCrop] = useState<any>({
    aspect: 1 / 1,
    width: 300,
    height: 300,
  });
  const [image, setImage] = useState<any>(null);

  useEffect(() => {
    if (image) {
      const canvas = document.createElement('canvas');
      const scaleX = image.naturalWidth / image.width;
      const scaleY = image.naturalHeight / image.height;
      canvas.width = crop.width;
      canvas.height = crop.height;
      const ctx: any = canvas.getContext('2d');

      ctx.drawImage(
        image,
        crop.x * scaleX,
        crop.y * scaleY,
        crop.width * scaleX,
        crop.height * scaleY,
        0,
        0,
        crop.width,
        crop.height
      );

      const base64Image = canvas.toDataURL('image/jpeg');
      const base64 = base64Image.split(',')[1];

      setResult(base64Image);
      setImageInput({ ...imageInput, type, base64 });
    }
  }, [crop]);

  return [crop, setCrop, setImage];
};

useImageSaveでは、useImageCropが返したimageInputとモーダル表示を更新するsetShowを受け取り、写真を登録してモーダルを閉じる関数saveCroppedImgを返すようにします。

モーダルの登録ボタンを押すとsaveCroppedImgが発火し、登録が完了します。

useImageSave.ts
export const useImageSave = (
  setShow: Dispatch<SetStateAction<boolean>>,
  imageInput: any
) => {
  const saveCroppedImg = async () => {
    try {
      console.log('imageInputを登録');
    } catch (err) {
      console.log(err);
    }

    setShow(false);
  };

  return [saveCroppedImg];
};

参考資料

8
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?