react-avatar-editor
Version:
Avatar / profile picture component. Resize and crop your uploaded image using a intuitive user interface.
449 lines (448 loc) • 15 kB
JavaScript
import e, { forwardRef as t, useCallback as n, useEffect as r, useImperativeHandle as i, useRef as a, useState as o } from "react";
//#region ../core/src/utils/drawRoundedRect.ts
var s = (e, t, n, r, i, a) => {
if (a === 0) e.rect(t, n, r, i);
else {
let o = r - a, s = i - a;
e.translate(t, n), e.arc(a, a, a, Math.PI, Math.PI * 1.5), e.lineTo(o, 0), e.arc(o, a, a, Math.PI * 1.5, Math.PI * 2), e.lineTo(r, s), e.arc(o, s, a, Math.PI * 2, Math.PI * .5), e.lineTo(a, i), e.arc(a, s, a, Math.PI * .5, Math.PI), e.closePath(), e.translate(-t, -n);
}
}, c = (e, t, n, r, i, a) => {
e.fillStyle = a;
let o = r / 3, s = i / 3;
e.fillRect(t, n, 1, i), e.fillRect(o + t, n, 1, i), e.fillRect(o * 2 + t, n, 1, i), e.fillRect(o * 3 + t, n, 1, i), e.fillRect(o * 4 + t, n, 1, i), e.fillRect(t, n, r, 1), e.fillRect(t, s + n, r, 1), e.fillRect(t, s * 2 + n, r, 1), e.fillRect(t, s * 3 + n, r, 1), e.fillRect(t, s * 4 + n, r, 1);
}, l = (e) => !!e.match(/^\s*data:([a-z]+\/[a-z]+(;[a-z-]+=[a-z-]+)?)?(;base64)?,[a-z0-9!$&',()*+;=\-._~:@/?%\s]*\s*$/i), u = (e, t) => new Promise((n, r) => {
let i = new Image();
i.addEventListener("load", () => n(i)), i.addEventListener("error", r), !l(e) && t && (i.crossOrigin = t), i.src = e;
}), d = (e) => new Promise((t, n) => {
let r = new FileReader();
r.addEventListener("load", (e) => {
try {
if (!e?.target?.result) throw Error("No image data");
t(u(e.target.result));
} catch (e) {
n(e);
}
}), r.readAsDataURL(e);
}), f = typeof File < "u", p = (e) => Math.PI / 180 * e, m = {
x: .5,
y: .5
}, h = class {
constructor(e) {
this.imageState = m, this.config = {
border: 25,
borderRadius: 0,
scale: 1,
rotate: 0,
color: [
0,
0,
0,
.5
],
backgroundColor: "",
borderColor: void 0,
showGrid: !1,
gridColor: "#666",
disableBoundaryChecks: !1,
disableHiDPIScaling: !1,
disableCanvasRotation: !0,
crossOrigin: void 0,
...e
}, this.pixelRatio = typeof window < "u" && window.devicePixelRatio && !this.config.disableHiDPIScaling ? window.devicePixelRatio : 1;
}
getPixelRatio() {
return this.pixelRatio;
}
getImageState() {
return this.imageState;
}
setImageState(e) {
this.imageState = e;
}
updateConfig(e) {
this.config = {
...this.config,
...e
};
}
isVertical() {
return !this.config.disableCanvasRotation && this.config.rotate % 180 != 0;
}
getBorders(e) {
let t = e ?? this.config.border;
return Array.isArray(t) ? t : [t, t];
}
getDimensions() {
let { width: e, height: t, rotate: n, border: r } = this.config, i = {
width: 0,
height: 0
}, [a, o] = this.getBorders(r);
return this.isVertical() ? (i.width = t, i.height = e) : (i.width = e, i.height = t), i.width += a * 2, i.height += o * 2, {
canvas: i,
rotate: n,
width: e,
height: t,
border: r
};
}
getXScale() {
if (!this.imageState.width || !this.imageState.height) throw Error("Image dimension is unknown.");
let e = this.config.width / this.config.height, t = this.imageState.width / this.imageState.height;
return Math.min(1, e / t);
}
getYScale() {
if (!this.imageState.width || !this.imageState.height) throw Error("Image dimension is unknown.");
let e = this.config.height / this.config.width, t = this.imageState.height / this.imageState.width;
return Math.min(1, e / t);
}
getCroppingRect(e) {
if (!this.imageState.width || !this.imageState.height) return {
x: 0,
y: 0,
width: 1,
height: 1
};
let t = e || {
x: this.imageState.x,
y: this.imageState.y
}, n = 1 / this.config.scale * this.getXScale(), r = 1 / this.config.scale * this.getYScale(), i = {
x: t.x - n / 2,
y: t.y - r / 2,
width: n,
height: r
}, a = 0, o = 1 - i.width, s = 0, c = 1 - i.height;
return (this.config.disableBoundaryChecks || n > 1 || r > 1) && (a = -i.width, o = 1, s = -i.height, c = 1), {
...i,
x: Math.max(a, Math.min(i.x, o)),
y: Math.max(s, Math.min(i.y, c))
};
}
getInitialSize(e, t) {
let n, r, i = this.getDimensions();
return i.height / i.width > t / e ? (n = i.height, r = n / t * e) : (r = i.width, n = r / e * t), {
height: n,
width: r
};
}
async loadImage(e) {
let t;
if (f && e instanceof File) t = await d(e);
else if (typeof e == "string") t = await u(e, this.config.crossOrigin);
else throw Error("Invalid image source");
let n = {
...this.getInitialSize(t.width, t.height),
resource: t,
x: .5,
y: .5
};
return this.imageState = n, n;
}
clearImage() {
this.imageState = m;
}
calculatePosition(e = this.imageState, t) {
let [n, r] = this.getBorders(t);
if (!e.width || !e.height) throw Error("Image dimension is unknown.");
let i = this.getCroppingRect(), a = e.width * this.config.scale, o = e.height * this.config.scale, s = -i.x * a, c = -i.y * o;
return this.isVertical() ? (s += r, c += n) : (s += n, c += r), {
x: s,
y: c,
height: o,
width: a
};
}
paint(e) {
e.save(), e.scale(this.pixelRatio, this.pixelRatio), e.translate(0, 0), e.fillStyle = "rgba(" + this.config.color.slice(0, 4).join(",") + ")";
let t = this.config.borderRadius, n = this.getDimensions(), [r, i] = this.getBorders(n.border), a = n.canvas.height, o = n.canvas.width;
t = Math.max(t, 0), t = Math.min(t, o / 2 - r, a / 2 - i), e.beginPath(), s(e, r, i, o - r * 2, a - i * 2, t), e.rect(o, 0, -o, a), e.fill("evenodd"), this.config.borderColor && (e.strokeStyle = "rgba(" + this.config.borderColor.slice(0, 4).join(",") + ")", e.lineWidth = 1, e.beginPath(), s(e, r + .5, i + .5, o - r * 2 - 1, a - i * 2 - 1, t), e.stroke()), this.config.showGrid && c(e, r, i, o - r * 2, a - i * 2, this.config.gridColor), e.restore();
}
paintImage(e, t, n, r = this.pixelRatio) {
if (!t.resource) return;
let i = this.calculatePosition(t, n);
e.save(), e.translate(e.canvas.width / 2, e.canvas.height / 2), e.rotate(this.config.rotate * Math.PI / 180), e.translate(-(e.canvas.width / 2), -(e.canvas.height / 2)), this.isVertical() && e.translate((e.canvas.width - e.canvas.height) / 2, (e.canvas.height - e.canvas.width) / 2), e.scale(r, r), e.globalCompositeOperation = "destination-over", e.drawImage(t.resource, i.x, i.y, i.width, i.height), this.config.backgroundColor && (e.fillStyle = this.config.backgroundColor, e.fillRect(0, 0, e.canvas.width, e.canvas.height)), e.restore();
}
getImage() {
let e = this.getCroppingRect(), t = this.imageState;
if (!t.resource) throw Error("No image resource available, please report this to: https://github.com/mosch/react-avatar-editor/issues");
e.x *= t.resource.width, e.y *= t.resource.height, e.width *= t.resource.width, e.height *= t.resource.height;
let n = document.createElement("canvas");
this.isVertical() ? (n.width = Math.round(e.height), n.height = Math.round(e.width)) : (n.width = Math.round(e.width), n.height = Math.round(e.height));
let r = n.getContext("2d");
if (!r) throw Error("No context found, please report this to: https://github.com/mosch/react-avatar-editor/issues");
return r.translate(n.width / 2, n.height / 2), r.rotate(this.config.rotate * Math.PI / 180), r.translate(-(n.width / 2), -(n.height / 2)), this.isVertical() && r.translate((n.width - n.height) / 2, (n.height - n.width) / 2), this.config.backgroundColor && (r.fillStyle = this.config.backgroundColor, r.fillRect(0, 0, n.width, n.height)), r.drawImage(t.resource, -e.x, -e.y), n;
}
getImageScaledToCanvas() {
let e = this.getDimensions(), t = this.imageState, n = document.createElement("canvas");
if (this.isVertical() ? (n.width = e.height, n.height = e.width) : (n.width = e.width, n.height = e.height), !t.resource) return n;
let r = n.getContext("2d");
if (!r) return n;
let i = this.calculatePosition(t, 0);
return r.save(), r.translate(n.width / 2, n.height / 2), r.rotate(this.config.rotate * Math.PI / 180), r.translate(-(n.width / 2), -(n.height / 2)), this.isVertical() && r.translate((n.width - n.height) / 2, (n.height - n.width) / 2), this.config.backgroundColor && (r.fillStyle = this.config.backgroundColor, r.fillRect(0, 0, n.width, n.height)), r.drawImage(t.resource, i.x, i.y, i.width, i.height), r.restore(), n;
}
calculateDragPosition(e, t, n, r) {
let i = n - e, a = r - t;
if (!this.imageState.width || !this.imageState.height) throw Error("Image dimension is unknown.");
let o = this.imageState.width * this.config.scale, s = this.imageState.height * this.config.scale, { x: c, y: l } = this.getCroppingRect();
c *= o, l *= s;
let u = this.config.rotate;
u %= 360, u = u < 0 ? u + 360 : u;
let d = Math.cos(p(u)), f = Math.sin(p(u)), m = c + i * d + a * f, h = l + -i * f + a * d, g = 1 / this.config.scale * this.getXScale(), _ = 1 / this.config.scale * this.getYScale();
return {
x: m / o + g / 2,
y: h / s + _ / 2
};
}
}, g = () => {}, _ = () => {
let e = !1;
try {
let t = Object.defineProperty({}, "passive", { get: function() {
e = !0;
} });
window.addEventListener("test", g, t), window.removeEventListener("test", g, t);
} catch {
e = !1;
}
return e;
}, v = t((t, s) => {
let { scale: c = 1, rotate: l = 0, border: u = 25, borderRadius: d = 0, width: f = 200, height: p = 200, color: m = [
0,
0,
0,
.5
], showGrid: g = !1, gridColor: v = "#666", disableBoundaryChecks: y = !1, disableHiDPIScaling: b = !1, disableCanvasRotation: x = !0, image: S, position: C, backgroundColor: w, crossOrigin: T, onLoadStart: E, onLoadFailure: D, onLoadSuccess: O, onImageReady: k, onImageChange: A, onMouseUp: j, onMouseMove: M, onPositionChange: N, borderColor: P, style: ee } = t, F = a(null), I = a(new h({
width: f,
height: p,
border: u,
borderRadius: d,
scale: c,
rotate: l,
color: m,
backgroundColor: w,
borderColor: P,
showGrid: g,
gridColor: v,
disableBoundaryChecks: y,
disableHiDPIScaling: b,
disableCanvasRotation: x,
crossOrigin: T
})), L = a(!1), R = a(void 0), z = a(void 0), [te, B] = o(!1), [V, H] = o(!1), [U, W] = o(I.current.getImageState()), G = a(j);
G.current = j;
let K = a(M);
K.current = M;
let q = a(N);
q.current = N, r(() => {
I.current.updateConfig({
width: f,
height: p,
border: u,
borderRadius: d,
scale: c,
rotate: l,
color: m,
backgroundColor: w,
borderColor: P,
showGrid: g,
gridColor: v,
disableBoundaryChecks: y,
disableHiDPIScaling: b,
disableCanvasRotation: x,
crossOrigin: T
});
}, [
f,
p,
u,
d,
c,
l,
m,
w,
P,
g,
v,
y,
b,
x,
T
]);
let J = n(() => {
if (!F.current) throw Error("No canvas found, please report this to: https://github.com/mosch/react-avatar-editor/issues");
return F.current;
}, []), Y = n(() => {
let e = J().getContext("2d");
if (!e) throw Error("No context found, please report this to: https://github.com/mosch/react-avatar-editor/issues");
return e;
}, [J]), X = n(async (e) => {
H(!0), E?.();
try {
let t = await I.current.loadImage(e);
L.current = !1, B(!1), W(t), k?.(), O?.(t);
} catch {
D?.();
} finally {
H(!1);
}
}, [
E,
k,
O,
D
]), ne = n(() => {
let e = J();
Y().clearRect(0, 0, e.width, e.height), I.current.clearImage(), W(I.current.getImageState());
}, [J, Y]), Z = n(() => {
let e = Y(), t = J();
e.clearRect(0, 0, t.width, t.height), I.current.paint(e), I.current.paintImage(e, U, u);
}, [
Y,
J,
U,
u,
f,
p,
d,
c,
l,
m,
w,
P,
g,
v,
y,
b,
x,
T
]), re = n((e) => {
e.preventDefault(), L.current = !0, R.current = void 0, z.current = void 0, B(!0);
}, []), ie = n(() => {
L.current = !0, R.current = void 0, z.current = void 0, B(!0);
}, []);
i(s, () => ({
getImage: () => I.current.getImage(),
getImageScaledToCanvas: () => I.current.getImageScaledToCanvas(),
getCroppingRect: () => I.current.getCroppingRect()
}), []), r(() => {
let e = Y();
S && X(S), I.current.paint(e);
let t = (e) => {
if (!L.current) return;
e.cancelable && e.preventDefault();
let t = "targetTouches" in e ? e.targetTouches[0].pageX : e.clientX, n = "targetTouches" in e ? e.targetTouches[0].pageY : e.clientY, r = R.current, i = z.current;
if (R.current = t, z.current = n, r !== void 0 && i !== void 0) {
let e = I.current.getImageState();
if (e.width && e.height) {
let a = I.current.calculateDragPosition(t, n, r, i);
q.current?.(a);
let o = {
...e,
...a
};
I.current.setImageState(o), W(o);
}
}
K.current?.(e);
}, n = () => {
L.current && (L.current = !1, B(!1), G.current?.());
}, r = _() ? { passive: !1 } : !1;
return document.addEventListener("mousemove", t, r), document.addEventListener("mouseup", n, r), document.addEventListener("touchmove", t, r), document.addEventListener("touchend", n, r), () => {
document.removeEventListener("mousemove", t, !1), document.removeEventListener("mouseup", n, !1), document.removeEventListener("touchmove", t, !1), document.removeEventListener("touchend", n, !1);
};
}, []), r(() => {
S ? X(S) : !S && U.x !== .5 && U.y !== .5 && ne();
}, [
S,
f,
p,
w
]), r(() => {
Z();
}, [Z]), r(() => {
if (!V) return;
let e = F.current;
if (!e) return;
let t = e.getContext("2d");
if (!t) return;
let n, r = performance.now(), i = (a) => {
let o = (a - r) / 1e3, s = .03 + Math.sin(o * 2.5) * .02 + .02;
t.save(), t.clearRect(0, 0, e.width, e.height), t.fillStyle = `rgba(255,255,255,${s})`, t.fillRect(0, 0, e.width, e.height), t.restore(), n = requestAnimationFrame(i);
};
return n = requestAnimationFrame(i), () => cancelAnimationFrame(n);
}, [V]);
let Q = a({
image: S,
width: f,
height: p,
position: C,
scale: c,
rotate: l,
imageX: U.x,
imageY: U.y
});
r(() => {
let e = Q.current;
(e.image !== S || e.width !== f || e.height !== p || e.position !== C || e.scale !== c || e.rotate !== l || e.imageX !== U.x || e.imageY !== U.y) && (A?.(), Q.current = {
image: S,
width: f,
height: p,
position: C,
scale: c,
rotate: l,
imageX: U.x,
imageY: U.y
});
}, [
S,
f,
p,
C,
c,
l,
U.x,
U.y,
A
]);
let $ = I.current.getDimensions(), ae = I.current.getPixelRatio(), oe = {
width: $.canvas.width,
height: $.canvas.height,
cursor: te ? "grabbing" : "grab",
touchAction: "none",
maxWidth: "none",
maxHeight: "none"
};
return e.createElement("canvas", {
width: $.canvas.width * ae,
height: $.canvas.height * ae,
onMouseDown: re,
onTouchStart: ie,
style: {
...oe,
...ee
},
ref: F
});
});
v.displayName = "AvatarEditor";
function y() {
let e = a(null);
return {
ref: e,
getImage: () => {
try {
return e.current?.getImage() ?? null;
} catch {
return null;
}
},
getImageScaledToCanvas: () => {
try {
return e.current?.getImageScaledToCanvas() ?? null;
} catch {
return null;
}
},
getCroppingRect: () => e.current?.getCroppingRect() ?? null
};
}
//#endregion
export { v as default, y as useAvatarEditor };
//# sourceMappingURL=index.mjs.map