トリミング

1import classNames from 'classnames';
2import 'cropperjs/dist/cropper.css';
3import React, { ChangeEvent, useRef, useState } from 'react';
4import { Cropper, ReactCropperElement } from 'react-cropper';
5
6const Crop = () => {
7  const [preview, setPreview] = useState<string>();
8  const [targetImage, setTargetImage] = useState<string | null>(
9    '/images/crop-placeholder.jpg'
10  );
11  const cropperRef = useRef<ReactCropperElement>(null);
12  const zoomSliderRef = useRef<HTMLInputElement>(null);
13  const [minZoom, setMinZoom] = useState<number>();
14
15  const setDefaultZoom = (event: Cropper.ReadyEvent<HTMLImageElement>) => {
16    const cropper = cropperRef.current?.cropper;
17    if (cropper) {
18      const canvasData = cropper.getCanvasData();
19      const zoomRatio = canvasData.width / canvasData.naturalWidth;
20      setMinZoom(zoomRatio);
21
22      if (zoomSliderRef.current) {
23        zoomSliderRef.current.value = zoomRatio.toFixed(4);
24      }
25    }
26  };
27
28  const crop = () => {
29    const imageDataURL = cropperRef.current?.cropper
30      .getCroppedCanvas({
31        width: 400,
32        height: 400,
33      })
34      .toDataURL();
35    setPreview(imageDataURL as string);
36  };
37
38  const setImage = (event: ChangeEvent<HTMLInputElement>) => {
39    const reader = new FileReader();
40
41    reader.onload = () => {
42      setTargetImage(reader.result as string);
43    };
44
45    const file = event.currentTarget.files?.[0];
46
47    if (file) {
48      reader.readAsDataURL(file);
49      event.currentTarget.value = '';
50    }
51  };
52
53  const changeZoom = (event: ChangeEvent<HTMLInputElement>) => {
54    const cropper = cropperRef.current?.cropper;
55    if (cropper) {
56      cropper.zoomTo(Number(event.currentTarget.value));
57    }
58  };
59
60  return (
61    <div className="grid grid-cols-1 lg:grid-cols-2 rounded-lg overflow-hidden">
62      <div className="flex flex-col h-72 lg:h-auto bg-black/60">
63        <div className="py-3 px-2">
64          <input
65            className="text-sm"
66            type="file"
67            accept="image/png,image/jpeg"
68            onChange={setImage}
69          />
70        </div>
71        <div className="flex-1 relative">
72          {targetImage && (
73            <Cropper
74              src={targetImage}
75              ready={setDefaultZoom}
76              className="absolute inset-0"
77              initialAspectRatio={1 / 1}
78              aspectRatio={1 / 1}
79              dragMode="move"
80              guides={false}
81              ref={cropperRef}
82              cropBoxMovable={false}
83              cropBoxResizable={false}
84              toggleDragModeOnDblclick={false}
85              center={false}
86              background={false}
87              viewMode={1}
88              autoCropArea={1}
89              zoomOnWheel={false}
90            />
91          )}
92        </div>
93        {targetImage && (
94          <div className="flex items-center space-x-4 p-2 bg-black">
95            <input
96              className="flex-1 min-w-0"
97              type="range"
98              min={minZoom}
99              max={(minZoom || 0) + 1}
100              step={0.0001}
101              ref={zoomSliderRef}
102              onChange={changeZoom}
103            />
104            <span className="lg:flex-1"></span>
105            <button
106              className="opacity-60 text-sm whitespace-nowrap"
107              onClick={() => {
108                setTargetImage(null);
109                setPreview(undefined);
110              }}
111            >
112              リセット
113            </button>
114            <button
115              className="text-sm p-2 rounded bg-pink-600 text-white whitespace-nowrap"
116              onClick={crop}
117            >
118              切り抜く
119            </button>
120          </div>
121        )}
122      </div>
123      <div className="bg-slate-900 p-4 lg:p-10">
124        <div
125          className={classNames(
126            'mx-auto lg:w-80 border-dashed overflow-hidden aspect-square rounded-md border-slate-700',
127            preview ? 'shadow' : 'border-2 bg-slate-800/10'
128          )}
129        >
130          {preview && <img src={preview} alt="" />}
131        </div>
132      </div>
133    </div>
134  );
135};
136
137export default Crop;