UNPKG

react-photo-editor

Version:

React component and hook for image editing, offering controls for brightness, contrast, saturation, and grayscale, along with features to rotate, flip, pan, draw, and zoom photos.

697 lines (696 loc) 28.1 kB
import { jsx as t, Fragment as we, jsxs as i } from "react/jsx-runtime"; import { useRef as fe, useState as s, useEffect as ye } from "react"; import './index.css';const be = ({ file: c, defaultBrightness: T = 100, defaultContrast: f = 100, defaultSaturate: G = 100, defaultGrayscale: J = 0, defaultFlipHorizontal: q = !1, defaultFlipVertical: A = !1, defaultZoom: K = 1, defaultRotate: D = 0, defaultLineColor: y = "#000000", defaultLineWidth: Q = 2, defaultMode: pe = "pan" }) => { const p = fe(null), w = fe(new Image()), [L, he] = s(""), [o, _] = s(T), [S, ee] = s(f), [V, re] = s(G), [Y, te] = s(J), [I, ae] = s(D), [x, ne] = s(q), [b, oe] = s(A), [u, z] = s(K), [M, v] = s(!1), [P, B] = s(null), [k, H] = s(0), [N, X] = s(0), [W, ie] = s(pe), [U, O] = s(null), [E, j] = s(y), [$, se] = s(Q), m = fe([]); ye(() => { if (c) { const e = URL.createObjectURL(c); return he(e), () => { URL.revokeObjectURL(e); }; } }, [c]), ye(() => { R(); }, [ c, L, I, x, b, u, o, S, V, Y, k, N ]); const me = (e) => { m.current.forEach(({ path: r, color: n, width: l }) => { e.beginPath(), e.strokeStyle = n, e.lineWidth = l, e.lineCap = "round", e.lineJoin = "round", r.forEach((d, h) => { h === 0 ? e.moveTo(d.x, d.y) : e.lineTo(d.x, d.y); }), e.stroke(); }); }, R = () => { if (!L) return; const e = p.current, r = e == null ? void 0 : e.getContext("2d"), n = w.current; w.current.src = L, w.current.onload = R, n.onload = () => { if (e && r) { const l = n.width * u, d = n.height * u, h = (n.width - l) / 2, g = (n.height - d) / 2; if (e.width = n.width, e.height = n.height, r.clearRect(0, 0, e.width, e.height), r.filter = C(), r.save(), I) { const F = e.width / 2, de = e.height / 2; r.translate(F, de), r.rotate(I * Math.PI / 180), r.translate(-F, -de); } x && (r.translate(e.width, 0), r.scale(-1, 1)), b && (r.translate(0, e.height), r.scale(1, -1)), r.translate(h + k, g + N), r.scale(u, u), r.drawImage(n, 0, 0, e.width, e.height), r.restore(), r.filter = "none", me(r); } }; }, ue = () => new Promise((e) => { const r = p.current; if (!r || !c) { e(null); return; } const n = (c.name.split(".").pop() || "").toLowerCase(); let l; switch (n) { case "jpg": case "jpeg": l = "image/jpeg"; break; case "png": l = "image/png"; break; default: l = "image/png"; } r.toBlob((d) => { if (d) { const h = new File([d], c.name, { type: d.type }); e(h); } else e(null); }, l); }), a = () => { const e = p.current; if (e && c) { const r = document.createElement("a"); r.download = c.name, r.href = e.toDataURL(c == null ? void 0 : c.type), r.click(); } }, C = () => `brightness(${o}%) contrast(${S}%) grayscale(${Y}%) saturate(${V}%)`, le = () => { z((e) => e + 0.1); }, ce = () => { z((e) => Math.max(e - 0.1, 0.1)); }; return { /** Reference to the canvas element. */ canvasRef: p, /** Source URL of the image being edited. */ imageSrc: L, /** Current brightness level. */ brightness: o, /** Current contrast level. */ contrast: S, /** Current saturation level. */ saturate: V, /** Current grayscale level. */ grayscale: Y, /** Current rotation angle in degrees. */ rotate: I, /** Flag indicating if the image is flipped horizontally. */ flipHorizontal: x, /** Flag indicating if the image is flipped vertically. */ flipVertical: b, /** Current zoom level. */ zoom: u, /** Flag indicating if the image is being dragged. */ isDragging: M, /** Starting coordinates for panning. */ panStart: P, /** Current horizontal offset for panning. */ offsetX: k, /** Current vertical offset for panning. */ offsetY: N, /** Current mode ('pan' or 'draw') */ mode: W, /** Current line color. */ lineColor: E, /** Current line width. */ lineWidth: $, /** Function to set the brightness level. */ setBrightness: _, /** Function to set the contrast level. */ setContrast: ee, /** Function to set the saturation level. */ setSaturate: re, /** Function to set the grayscale level. */ setGrayscale: te, /** Function to set the rotation angle. */ setRotate: ae, /** Function to set the horizontal flip state. */ setFlipHorizontal: ne, /** Function to set the vertical flip state. */ setFlipVertical: oe, /** Function to set the zoom level. */ setZoom: z, /** Function to set the dragging state. */ setIsDragging: v, /** Function to set the starting coordinates for panning. */ setPanStart: B, /** Function to set the horizontal offset for panning. */ setOffsetX: H, /** Function to set the vertical offset for panning. */ setOffsetY: X, /** Function to zoom in. */ handleZoomIn: le, /** Function to zoom out. */ handleZoomOut: ce, /** Function to handle pointer down events. */ handlePointerDown: (e) => { if (W === "draw") { const r = p.current; if (!r) return; const n = r.getBoundingClientRect(), l = r.width / n.width, d = r.height / n.height, h = (e.clientX - n.left) * l, g = (e.clientY - n.top) * d; O({ x: h, y: g }), m.current.push({ path: [{ x: h, y: g }], color: E, width: $ }); } else { v(!0); const r = e.clientX - (x ? -k : k), n = e.clientY - (b ? -N : N); B({ x: r, y: n }); } }, /** Function to handle pointer up events. */ handlePointerUp: () => { v(!1), O(null); }, /** Function to handle pointer move events. */ handlePointerMove: (e) => { if (W === "draw" && U) { const r = p.current, n = r == null ? void 0 : r.getContext("2d"), l = r == null ? void 0 : r.getBoundingClientRect(); if (!r || !n || !l) return; const d = r.width / l.width, h = r.height / l.height, g = (e.clientX - l.left) * d, F = (e.clientY - l.top) * h, de = m.current[m.current.length - 1].path; n.strokeStyle = E, n.lineWidth = $, n.lineCap = "round", n.lineJoin = "round", n.beginPath(), n.moveTo(U.x, U.y), n.lineTo(g, F), n.stroke(), O({ x: g, y: F }), de.push({ x: g, y: F }); return; } if (M && P) { e.preventDefault(); const r = e.clientX - P.x, n = e.clientY - P.y; H(x ? -r : r), X(b ? -n : n); } }, /** Function to handle wheel events for zooming. */ handleWheel: (e) => { e.deltaY < 0 ? le() : ce(); }, /** Function to download the edited image. */ downloadImage: a, /** Function to generate the edited image file. */ generateEditedFile: ue, /** Function to reset filters and styles to default. */ resetFilters: () => { _(T), ee(f), re(G), te(J), ae(D), ne(q), oe(A), z(K), j(y), se(Q), m.current = [], H(0), X(0), B(null), v(!1), ie("pan"), R(); }, /** Function to apply filters and transformations. */ applyFilter: R, /** Function to set the mode. */ setMode: ie, /** Function to set the line color. */ setLineColor: j, /** Function to set the line width. */ setLineWidth: se }; }, xe = "rpe-text-gray-900 rpe-bg-white rpe-border rpe-border-gray-300 rpe-ml-2 focus:rpe-outline-hidden hover:rpe-bg-gray-100 focus:rpe-ring-4 focus:rpe-ring-gray-100 rpe-font-medium rpe-rounded-full rpe-text-sm rpe-px-2 rpe-py-1 dark:rpe-bg-gray-800 dark:rpe-text-white dark:rpe-border-gray-600 dark:hover:rpe-bg-gray-700 dark:hover:rpe-border-gray-600 dark:focus:rpe-ring-gray-700", Ie = ({ file: c, onSaveImage: T, allowColorEditing: f = !0, allowFlip: G = !0, allowRotate: J = !0, allowZoom: q = !0, allowDrawing: A = !0, downloadOnSave: K, open: D, onClose: y, modalHeight: Q, modalWidth: pe, canvasHeight: p, canvasWidth: w, maxCanvasHeight: L, maxCanvasWidth: he, labels: o = { close: "Close", save: "Save", rotate: "Rotate", brightness: "Brightness", contrast: "Contrast", saturate: "Saturate", grayscale: "Grayscale", reset: "Reset photo", flipHorizontal: "Flip photo horizontally", flipVertical: "Flip photo vertically", zoomIn: "Zoom in", zoomOut: "Zoom out", draw: "Draw", brushColor: "Choose brush color", brushWidth: "Choose brush width" } }) => { const { canvasRef: _, brightness: S, setBrightness: ee, contrast: V, setContrast: re, saturate: Y, setSaturate: te, grayscale: I, setGrayscale: ae, rotate: x, setRotate: ne, flipHorizontal: b, setFlipHorizontal: oe, flipVertical: u, setFlipVertical: z, mode: M, setMode: v, setLineColor: P, lineColor: B, setLineWidth: k, lineWidth: H, isDragging: N, handlePointerDown: X, handlePointerUp: W, handlePointerMove: ie, handleWheel: U, handleZoomIn: O, handleZoomOut: E, resetFilters: j, downloadImage: $, generateEditedFile: se } = be({ file: c }); ye(() => { j(); }, [D]); const m = (a, C, le, ce) => { var ge; const Z = parseInt((ge = a.target) == null ? void 0 : ge.value); !isNaN(Z) && Z >= le && Z <= ce && C(Z); }, me = [ { name: o.rotate, value: x, setValue: ne, min: -180, max: 180, type: "range", id: "rotateInput", "aria-labelledby": "rotateInputLabel", hide: !J }, { name: o.brightness, value: S, setValue: ee, min: 0, max: 200, type: "range", id: "brightnessInput", "aria-labelledby": "brightnessInputLabel", hide: !f }, { name: o.contrast, value: V, setValue: re, min: 0, max: 200, type: "range", id: "contrastInput", "aria-labelledby": "contrastInputLabel", hide: !f }, { name: o.saturate, value: Y, setValue: te, min: 0, max: 200, type: "range", id: "saturateInput", "aria-labelledby": "saturateInputLabel", hide: !f }, { name: o.grayscale, value: I, setValue: ae, min: 0, max: 100, type: "range", id: "grayscaleInput", "aria-labelledby": "grayscaleInputLabel", hide: !f } ], R = () => { j(), y && y(); }, ue = async () => { K && $(); const a = await se(); a && T(a), y && y(); }; return /* @__PURE__ */ t(we, { children: D && /* @__PURE__ */ i(we, { children: [ /* @__PURE__ */ t( "div", { "data-testid": "photo-editor-main", className: "photo-editor-main rpe-justify-center rpe-items-center rpe-flex rpe-overflow-auto rpe-fixed rpe-inset-0 rpe-z-50", children: /* @__PURE__ */ i( "div", { style: { height: Q ?? "38rem", width: pe ?? "40rem" }, id: "photo-editor-modal", className: "rpe-relative rpe-rounded-lg rpe-shadow-lg rpe-max-sm:w-[22rem] rpe-bg-white dark:rpe-bg-[#1e1e1e]", children: [ /* @__PURE__ */ i("div", { className: "rpe-flex rpe-justify-end rpe-p-2 rpe-rounded-t", children: [ /* @__PURE__ */ t("button", { className: xe, onClick: R, type: "button", children: o.close }), /* @__PURE__ */ t( "button", { className: xe, onClick: () => void ue(), type: "button", "data-testid": "save-button", children: o.save } ) ] }), /* @__PURE__ */ t("div", { className: "rpe-p-2", children: /* @__PURE__ */ i("div", { className: "rpe-flex rpe-flex-col", children: [ /* @__PURE__ */ t( "canvas", { style: { width: w ?? "auto", height: p ?? "auto", maxHeight: L ?? "22rem", maxWidth: he ?? "36rem" }, className: `rpe-canvas rpe-touch-none rpe-border dark:rpe-border-gray-700 rpe-object-fill rpe-mx-auto ${M === "draw" ? "rpe-cursor-crosshair" : N ? "rpe-cursor-grabbing" : "rpe-cursor-grab"}`, "data-testid": "image-editor-canvas", id: "rpe-canvas", ref: _, onPointerDown: X, onPointerMove: ie, onPointerUp: W, onPointerLeave: W, onWheel: U, width: typeof w == "number" ? w : void 0, height: typeof p == "number" ? p : void 0 } ), /* @__PURE__ */ t("div", { className: "rpe-items-center rpe-flex rpe-m-1 rpe-flex-col", children: /* @__PURE__ */ t("div", { className: "rpe-flex rpe-flex-col rpe-bottom-12 rpe-gap-1 rpe-mt-4 rpe-max-sm:w-72 rpe-w-11/12 rpe-absolute ", children: me.map( (a) => !a.hide && /* @__PURE__ */ i( "div", { className: "rpe-flex rpe-flex-row rpe-items-center", children: [ /* @__PURE__ */ i( "label", { id: `${a.name}InputLabel`, className: "rpe-text-xs rpe-font-medium rpe-text-gray-900 dark:rpe-text-white rpe-w-10", htmlFor: a.id, children: [ a.name[0].toUpperCase() + a.name.slice(1), ":", " " ] } ), /* @__PURE__ */ t( "input", { id: a.id, type: a.type, value: a.value, step: "1", onChange: (C) => m(C, a.setValue, a.min, a.max), min: a.min, max: a.max, className: "rpe-ml-[1.7rem] rpe-w-full rpe-h-1 rpe-bg-gray-200 rpe-rounded-lg rpe-appearance-none rpe-cursor-pointer rpe-range-sm dark:rpe-bg-gray-700", "aria-labelledby": `${a.name}InputLabel`, "aria-valuemin": a.min, "aria-valuemax": a.max, "aria-valuenow": a.value } ), /* @__PURE__ */ t( "input", { type: "number", value: a.value, onChange: (C) => m(C, a.setValue, a.min, a.max), min: a.min, max: a.max, className: "rpe-w-14 rpe-ml-2 rpe-rounded-md rpe-text-right rpe-bg-gray-100 rpe-text-black dark:rpe-bg-gray-700 dark:rpe-text-white", "aria-labelledby": `${a.name}InputLabel`, "aria-valuemin": a.min, "aria-valuemax": a.max, "aria-valuenow": a.value } ) ] }, a.name ) ) }) }), /* @__PURE__ */ t("div", { className: "rpe-flex rpe-justify-center rpe-items-center rpe-content-center", children: /* @__PURE__ */ i("div", { className: "rpe-absolute rpe-bottom-0 rpe-mt-2 rpe-flex rpe-items-center", children: [ /* @__PURE__ */ t( "button", { title: o.reset, className: "rpe-mx-1 focus:rpe-ring-2 focus:rpe-ring-gray-300 dark:focus:rpe-ring-gray-700 rpe-rounded-md rpe-p-1", onClick: j, "aria-label": o.reset, type: "button", children: /* @__PURE__ */ i( "svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: "lucide lucide-rotate-ccw dark:rpe-stroke-slate-200", children: [ /* @__PURE__ */ t("path", { d: "M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" }), /* @__PURE__ */ t("path", { d: "M3 3v5h5" }) ] } ) } ), G && /* @__PURE__ */ i("div", { className: "rpe-inline-block", "data-testid": "flip-btns", children: [ /* @__PURE__ */ t( "button", { className: "rpe-mx-1 focus:rpe-ring-2 focus:rpe-ring-gray-300 dark:focus:rpe-ring-gray-700 rpe-rounded-md rpe-p-1", onClick: () => oe(!b), type: "button", title: o.flipHorizontal, "aria-label": o.flipHorizontal, children: /* @__PURE__ */ i( "svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: "lucide lucide-flip-horizontal dark:rpe-stroke-slate-200", children: [ /* @__PURE__ */ t("path", { d: "M8 3H5a2 2 0 0 0-2 2v14c0 1.1.9 2 2 2h3" }), /* @__PURE__ */ t("path", { d: "M16 3h3a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-3" }), /* @__PURE__ */ t("path", { d: "M12 20v2" }), /* @__PURE__ */ t("path", { d: "M12 14v2" }), /* @__PURE__ */ t("path", { d: "M12 8v2" }), /* @__PURE__ */ t("path", { d: "M12 2v2" }) ] } ) } ), /* @__PURE__ */ t( "button", { className: "rpe-mx-1 focus:rpe-ring-2 focus:rpe-ring-gray-300 dark:focus:rpe-ring-gray-700 rpe-rounded-md rpe-p-1", onClick: () => z(!u), type: "button", title: o.flipVertical, "aria-label": o.flipVertical, children: /* @__PURE__ */ i( "svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: "lucide lucide-flip-vertical dark:rpe-stroke-slate-200", children: [ /* @__PURE__ */ t("path", { d: "M21 8V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v3" }), /* @__PURE__ */ t("path", { d: "M21 16v3a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-3" }), /* @__PURE__ */ t("path", { d: "M4 12H2" }), /* @__PURE__ */ t("path", { d: "M10 12H8" }), /* @__PURE__ */ t("path", { d: "M16 12h-2" }), /* @__PURE__ */ t("path", { d: "M22 12h-2" }) ] } ) } ) ] }), q && /* @__PURE__ */ i("div", { className: "rpe-inline-block", "data-testid": "zoom-btns", children: [ /* @__PURE__ */ t( "button", { "data-testid": "zoom-in", type: "button", className: "rpe-mx-1 focus:rpe-ring-2 focus:rpe-ring-gray-300 dark:focus:rpe-ring-gray-700 rpe-rounded-md rpe-p-1", onClick: O, title: o.zoomIn, "aria-label": o.zoomIn, children: /* @__PURE__ */ i( "svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: "lucide lucide-zoom-in dark:rpe-stroke-slate-200", children: [ /* @__PURE__ */ t("circle", { cx: "11", cy: "11", r: "8" }), /* @__PURE__ */ t("line", { x1: "21", x2: "16.65", y1: "21", y2: "16.65" }), /* @__PURE__ */ t("line", { x1: "11", x2: "11", y1: "8", y2: "14" }), /* @__PURE__ */ t("line", { x1: "8", x2: "14", y1: "11", y2: "11" }) ] } ) } ), /* @__PURE__ */ t( "button", { "data-testid": "zoom-out", type: "button", className: "rpe-mx-1 focus:rpe-ring-2 focus:rpe-ring-gray-300 dark:focus:rpe-ring-gray-700 rpe-rounded-md rpe-p-1", onClick: E, title: o.zoomOut, "aria-label": o.zoomOut, children: /* @__PURE__ */ i( "svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: "lucide lucide-zoom-out dark:rpe-stroke-slate-200", children: [ /* @__PURE__ */ t("circle", { cx: "11", cy: "11", r: "8" }), /* @__PURE__ */ t("line", { x1: "21", x2: "16.65", y1: "21", y2: "16.65" }), /* @__PURE__ */ t("line", { x1: "8", x2: "14", y1: "11", y2: "11" }) ] } ) } ) ] }), A && /* @__PURE__ */ i("div", { className: "rpe-flex rpe-items-center", children: [ /* @__PURE__ */ t( "button", { "data-testid": "draw-btn", type: "button", className: "rpe-mx-1 focus:rpe-ring-2 focus:rpe-ring-gray-300 dark:focus:rpe-ring-gray-700 rpe-rounded-md rpe-p-1", onClick: () => M == "pan" ? v("draw") : v("pan"), title: o.draw, "aria-label": o.draw, children: /* @__PURE__ */ i( "svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: "lucide lucide-brush dark:rpe-stroke-slate-200", children: [ /* @__PURE__ */ t("path", { d: "m9.06 11.9 8.07-8.06a2.85 2.85 0 1 1 4.03 4.03l-8.06 8.08" }), /* @__PURE__ */ t("path", { d: "M7.07 14.94c-1.66 0-3 1.35-3 3.02 0 1.33-2.5 1.52-2 2.02 1.08 1.1 2.49 2.02 4 2.02 2.2 0 4-1.8 4-4.04a3.01 3.01 0 0 0-3-3.02z" }) ] } ) } ), M == "draw" && /* @__PURE__ */ i("div", { className: "rpe-flex rpe-items-center dark:rpe-bg-zinc-600 rpe-bg-zinc-200 rpe-p-1 rpe-rounded-md", children: [ /* @__PURE__ */ t( "input", { type: "color", onChange: (a) => P(a.target.value), className: "rpe-mx-1 rpe-w-6 rpe-h-6 rpe-rounded-sm focus:rpe-ring-2 focus:rpe-ring-gray-300 dark:focus:rpe-ring-gray-700", id: "rpe-brush-color", value: B, title: o.brushColor, "aria-label": o.brushColor } ), /* @__PURE__ */ t( "input", { type: "number", title: o.brushWidth, "aria-label": o.brushWidth, onChange: (a) => m(a, k, 2, 100), className: "rpe-w-12 rpe-ml-2 rpe-rounded-md rpe-text-right rpe-bg-gray-100 rpe-text-black dark:rpe-bg-gray-700 dark:rpe-text-white rpe-mx-1 rpe-p-0 rpe-text-sm", id: "rpe-brush-width", value: H, min: 2, max: 100 } ) ] }) ] }) ] }) }) ] }) }) ] } ) } ), /* @__PURE__ */ t("div", { className: "rpe-opacity-75 rpe-fixed rpe-inset-0 rpe-z-40 rpe-bg-black" }) ] }) }); }; export { Ie as ReactPhotoEditor, be as usePhotoEditor };