vue-pincher
Version:
Vue 3 image cropper component
347 lines (346 loc) • 11.1 kB
JavaScript
import { Transition as e, createCommentVNode as t, createElementBlock as n, createElementVNode as r, createVNode as i, defineComponent as a, mergeModels as o, onMounted as s, onUnmounted as c, openBlock as l, useModel as u, useTemplateRef as d, watch as f, withCtx as p } from "vue";
//#region lib/crop.ts
function m(e, t, n, r = !1) {
let i = {
x: e.x + e.width / 2,
y: e.y + e.height / 2
}, a = h(t, n), o = n.width / a, s = n.height / a;
if (r) {
let e = i.x < t.width / 2 ? 1 / 3 : 2 / 3;
return {
x: i.x - o * e,
y: i.y - s * e,
width: o,
height: s
};
} else return {
x: i.x - o / 2,
y: i.y - s / 2,
width: o,
height: s
};
}
function h(e, t) {
return Math.max(t.width / e.width, t.height / e.height);
}
function g(e, t) {
return Math.min(t.width / e.width, t.height / e.height);
}
function _(e) {
return e = (e % 360 + 360) % 360, e >= 180 && (e -= 360), e;
}
function v(e) {
return Math.PI / 180 * e;
}
function y(e) {
let t = 0, n = 0;
for (let r of e.touches) t += r.clientX, n += r.clientY;
let r = e.touches.length;
return {
x: t / r,
y: n / r
};
}
function b(e) {
if (!e.touches?.[0] || !e.touches?.[1]) throw Error("getDistance requires at least two touches");
let t = e.touches[0], n = e.touches[1], r = n.clientX - t.clientX, i = n.clientY - t.clientY;
return Math.hypot(r, i);
}
//#endregion
//#region lib/manipulator.ts
var x = class {
resizeObserver;
resetRequested = !1;
clientWidth = 0;
clientHeight = 0;
canvasWidth = 0;
canvasHeight = 0;
fitWidth = 0;
fitHeight = 0;
constructor(e, t, n, r, i) {
this._ctx = e, this._image = t, this._state = n, this._trimTo = r, this._attention = i, this.adjustSize(), this.resizeObserver = new ResizeObserver(() => {
e && requestAnimationFrame(() => {
this.adjustSize(), this.resetRequested ? (this.resetRequested = !1, this.reset()) : this.redraw();
});
}), this.resizeObserver.observe(e.canvas), n.scale === 0 && this.reset();
}
set image(e) {
this._image = e, this.redraw();
}
set state(e) {
this._state = e, this.redraw();
}
set attention(e) {
this._attention = e, this.reset();
}
set trimTo(e) {
this._trimTo = e, this.reset();
}
reset(e = !1) {
if (e) {
this.resetRequested = !0;
return;
}
if (this._trimTo) this.showRegion(this._trimTo, !0);
else if (this._attention?.[0]) this.showRegion(m(this._attention[0], this._image, {
width: this.canvasWidth,
height: this.canvasHeight
}, !1));
else {
let e = v(this._state.angle), t = Math.abs(Math.cos(e)), n = Math.abs(Math.sin(e)), r = this.fitWidth * t + this.fitHeight * n, i = this.fitHeight * t + this.fitWidth * n;
this.manipulate({
scale: h({
width: r,
height: i
}, {
width: this.canvasWidth,
height: this.canvasHeight
}),
offsetX: 0,
offsetY: 0
});
}
}
manipulate(e) {
if (!this._ctx || Object.values(e).every((e) => e === void 0)) return;
let t = !1;
if (e.angle !== void 0 || e.dAngle !== void 0) {
let n = _(e.angle ?? this._state.angle + (e.dAngle ?? 0));
this._state.angle !== n && (this._state.angle = n, t = !0);
}
let n = this._state.scale;
if (t || e.scale !== void 0 || e.dScale !== void 0 || e.offsetX !== void 0 || e.dOffsetX !== void 0 || e.offsetY !== void 0 || e.dOffsetY !== void 0) {
let r = v(this._state.angle), i = Math.abs(Math.cos(r)), a = Math.abs(Math.sin(r)), o = this.fitWidth * i + this.fitHeight * a, s = this.fitHeight * i + this.fitWidth * a, c = !1;
if (t || e.scale !== void 0 || e.dScale !== void 0) {
let n = Math.min(this.canvasWidth / o, this.canvasHeight / s), r = e.scale ?? this._state.scale * (e.dScale ?? 1), i = Math.max(n, r);
this._state.scale !== i && (this._state.scale = i, c = !0, t = !0);
}
if (c) {
let t = this._state.scale / n, r = e.centerX ?? this.canvasWidth / 2, i = e.centerY ?? this.canvasHeight / 2, a = this.canvasWidth / 2 + this._state.offsetX * this.canvasWidth, o = this.canvasHeight / 2 + this._state.offsetY * this.canvasHeight, s = r - a, c = i - o;
this._state.offsetX += (s - s * t) / this.canvasWidth, this._state.offsetY += (c - c * t) / this.canvasHeight;
}
if (t || e.offsetX !== void 0 || e.dOffsetX !== void 0) {
let n = e.offsetX ?? this._state.offsetX + (e.dOffsetX ?? 0) / this.clientWidth, r = Math.abs(this.canvasWidth - o * this._state.scale) / 2 / this.canvasWidth, i = Math.max(-r, Math.min(n, r));
this._state.offsetX !== i && (this._state.offsetX = i, t = !0);
}
if (t || e.offsetY !== void 0 || e.dOffsetY !== void 0) {
let n = e.offsetY ?? this._state.offsetY + (e.dOffsetY ?? 0) / this.clientHeight, r = Math.abs(this.canvasHeight - s * this._state.scale) / 2 / this.canvasHeight, i = Math.max(-r, Math.min(n, r));
this._state.offsetY !== i && (this._state.offsetY = i, t = !0);
}
}
t && this.redraw();
}
showRegion(e, t = !1) {
let n = g(this._image, {
width: this.canvasWidth,
height: this.canvasHeight
}), r = (t ? h : g)(e, {
width: this.canvasWidth,
height: this.canvasHeight
}), i = e.x + e.width / 2, a = e.y + e.height / 2, o = (this._image.width / 2 - i) * r / this.canvasWidth, s = (this._image.height / 2 - a) * r / this.canvasHeight;
this.manipulate({
scale: r / n,
offsetX: o,
offsetY: s
});
}
getCurrentRegion() {
let e = this._state.scale, t = g(this._image, {
width: this.canvasWidth,
height: this.canvasHeight
});
return {
x: (this._image.width * e * t - this.canvasWidth - 2 * this._state.offsetX * this.canvasWidth) / (e * t * 2),
y: (this._image.height * e * t - this.canvasHeight - 2 * this._state.offsetY * this.canvasHeight) / (e * t * 2),
width: this.canvasWidth / e / t,
height: this.canvasHeight / e / t
};
}
close() {
this.resizeObserver.disconnect();
}
redraw() {
if (!this._ctx) return;
let e = this._state.scale, t = this.fitWidth * e, n = this.fitHeight * e;
if (this._ctx.save(), this._ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight), this._ctx.translate(this._state.offsetX * this.canvasWidth, this._state.offsetY * this.canvasHeight), this._state.angle !== 0) {
let e = this.canvasWidth / 2, t = this.canvasHeight / 2;
this._ctx.translate(e, t), this._ctx.rotate(v(this._state.angle)), this._ctx.translate(-e, -t);
}
this._ctx.drawImage(this._image, (this.canvasWidth - t) / 2, (this.canvasHeight - n) / 2, t, n), this._ctx.restore();
}
adjustSize() {
if (!this._ctx) return;
this.clientWidth = this._ctx.canvas.clientWidth, this.clientHeight = this._ctx.canvas.clientHeight, this.canvasWidth = this._ctx.canvas.width = this.clientWidth * window.devicePixelRatio, this.canvasHeight = this._ctx.canvas.height = this.clientHeight * window.devicePixelRatio;
let e = g(this._image, {
width: this.canvasWidth,
height: this.canvasHeight
});
this.fitWidth = this._image.width * e, this.fitHeight = this._image.height * e;
}
}, S = class {
constructor(e) {
this.manipulator = e;
}
wheel(e) {
this.manipulator.manipulate({
dScale: Math.sign(e.deltaY) * -.1 + 1,
centerX: e.offsetX * window.devicePixelRatio,
centerY: e.offsetY * window.devicePixelRatio
});
}
dblclick() {
this.manipulator.reset();
}
manipulating = !1;
previousClick = null;
previousTouch = null;
mouseMoving(e) {
if (this.manipulating && this.previousClick) {
let t = e.clientX - this.previousClick.x, n = e.clientY - this.previousClick.y;
this.previousClick = e, this.manipulator.manipulate({
dOffsetX: t,
dOffsetY: n
});
}
}
mouseDown(e) {
this.manipulating = !0, this.previousClick = e;
}
mouseUp() {
this.manipulating = !1;
}
touchMoving(e) {
if (this.manipulating) {
if (e.touches?.[0] && this.previousTouch?.touches?.[0] && e.touches.length === 1) {
let t = e.touches[0].clientX - this.previousTouch.touches[0].clientX, n = e.touches[0].clientY - this.previousTouch.touches[0].clientY;
this.manipulator.manipulate({
dOffsetX: t,
dOffsetY: n
});
} else if (e.touches && e.touches.length >= 2 && this.previousTouch && this.previousTouch.touches.length >= 2) {
let t = y(this.previousTouch), n = y(e), r = e.target.getBoundingClientRect(), i = n.x - r.x, a = n.y - r.y;
this.manipulator.manipulate({
dScale: b(e) / b(this.previousTouch),
dOffsetX: n.x - t.x,
dOffsetY: n.y - t.y,
centerX: i * window.devicePixelRatio,
centerY: a * window.devicePixelRatio
});
}
e.touches && (this.previousTouch = e), e.preventDefault();
}
}
touchDown(e) {
e.touches && (this.manipulating = !0, this.previousTouch = e);
}
touchUp() {
this.manipulating = !1;
}
}, C = {
ref: "canvasRef",
class: "fit"
}, w = {
key: 0,
class: "grid-overlay"
}, T = /* @__PURE__ */ ((e, t) => {
let n = e.__vccOpts || e;
for (let [e, r] of t) n[e] = r;
return n;
})(/* @__PURE__ */ a({
__name: "vue-pincher",
props: /* @__PURE__ */ o({
image: {},
readonly: {
type: Boolean,
default: !1
},
showGrid: {
type: Boolean,
default: !1
},
trimTo: { default: () => void 0 },
attention: { default: () => [] }
}, {
modelValue: { default: {
angle: 0,
offsetX: 0,
offsetY: 0,
scale: 0
} },
modelModifiers: {}
}),
emits: ["update:modelValue"],
setup(a, { expose: o }) {
o({
rotate: y,
rotateTo: b,
reset: v,
showRegion: T
});
let m = u(a, "modelValue"), h = d("canvasRef"), g, _;
s(() => {
if (!h.value) return;
let e = h.value.getContext("2d");
e && (g = new x(e, a.image, m.value, a.trimTo, a.attention), a.readonly || (_ = new S(g)));
}), c(() => {
g?.close();
}), f(() => a.image, (e) => g.image = e), f(() => a.trimTo, (e) => g.trimTo = e), f(() => a.attention, (e) => g.attention = e), f(m, (e) => g.state = e), f(() => [
m.value.angle,
m.value.offsetX,
m.value.offsetY,
m.value.scale
], () => g.redraw());
function v(e = !1) {
g.reset(e);
}
function y(e) {
g.manipulate({ dAngle: e });
}
function b(e) {
g.manipulate({ angle: e });
}
function T(e) {
g.showRegion(e);
}
a.readonly || (window.addEventListener("touchmove", D), window.addEventListener("touchend", O), window.addEventListener("mousemove", A), window.addEventListener("mouseup", j), c(() => {
window.removeEventListener("touchmove", D), window.removeEventListener("touchend", O), window.removeEventListener("mousemove", A), window.removeEventListener("mouseup", j);
}));
function E(e) {
_?.touchDown(e);
}
function D(e) {
_?.touchMoving(e);
}
function O() {
_?.touchUp();
}
function k(e) {
_?.mouseDown(e);
}
function A(e) {
_?.mouseMoving(e);
}
function j() {
_?.mouseUp();
}
function M(e) {
_?.wheel(e);
}
function N() {
_?.dblclick();
}
return (o, s) => (l(), n("div", {
class: "fit",
style: { position: "relative" },
onMousedown: k,
onTouchstart: E,
onWheel: M,
onDblclick: N
}, [r("canvas", C, null, 512), i(e, null, {
default: p(() => [a.showGrid ? (l(), n("div", w)) : t("", !0)]),
_: 1
})], 32));
}
}), [["__scopeId", "data-v-5f0c7ad8"]]);
//#endregion
export { T as default };