vue-pincher
Version:
Vue 3 image cropper component
400 lines (399 loc) • 13.5 kB
JavaScript
var b = Object.defineProperty;
var O = (h, t, e) => t in h ? b(h, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : h[t] = e;
var u = (h, t, e) => O(h, typeof t != "symbol" ? t + "" : t, e);
import { defineComponent as C, mergeModels as k, useModel as S, useTemplateRef as D, onMounted as E, onUnmounted as Y, watch as m, createElementBlock as X, openBlock as M, createElementVNode as P, createVNode as z, Transition as L, withCtx as U, createCommentVNode as A } from "vue";
function I(h, t, e, o = !1) {
const s = {
x: h.x + h.width / 2,
y: h.y + h.height / 2
}, i = W(t, e), a = e.width / i, c = e.height / i;
if (o) {
const r = s.x < t.width / 2 ? 0.3333333333333333 : 0.6666666666666666;
return {
x: s.x - a * r,
y: s.y - c * r,
width: a,
height: c
};
} else
return {
x: s.x - a / 2,
y: s.y - c / 2,
width: a,
height: c
};
}
function W(h, t) {
return Math.max(t.width / h.width, t.height / h.height);
}
function p(h, t) {
return Math.min(t.width / h.width, t.height / h.height);
}
function V(h) {
return h = (h % 360 + 360) % 360, h >= 180 && (h -= 360), h;
}
function H(h) {
return h * (Math.PI / 180);
}
function y(h) {
let t = 0, e = 0;
for (const a of h.touches)
t += a.clientX, e += a.clientY;
const o = h.touches.length, s = t / o, i = e / o;
return { x: s, y: i };
}
function R(h) {
var i, a;
if (!((i = h.touches) != null && i[0]) || !((a = h.touches) != null && a[1]))
throw new Error("getDistance requires at least two touches");
const t = h.touches[0], e = h.touches[1], o = e.clientX - t.clientX, s = e.clientY - t.clientY;
return Math.hypot(o, s);
}
class q {
constructor(t, e, o, s, i) {
u(this, "resizeObserver");
u(this, "resetRequested", !1);
// Actual width and height of the canvas
u(this, "clientWidth", 0);
u(this, "clientHeight", 0);
// Canvas DPI-adjusted width width and height using to mitigate blurry images on high DPI screens (phones, tablets, etc.)
u(this, "canvasWidth", 0);
u(this, "canvasHeight", 0);
// Image width and height scaled to fit into canvas
u(this, "fitWidth", 0);
u(this, "fitHeight", 0);
this._ctx = t, this._image = e, this._state = o, this._trimTo = s, this._attention = i, this.adjustSize(), this.resizeObserver = new ResizeObserver(() => {
t && (this.adjustSize(), this.resetRequested ? (this.resetRequested = !1, this.reset()) : this.redraw());
}), this.resizeObserver.observe(t.canvas), o.scale === 0 && this.reset();
}
set image(t) {
this._image = t, this.redraw();
}
set state(t) {
this._state = t, this.redraw();
}
set attention(t) {
this._attention = t, this.reset();
}
set trimTo(t) {
this._trimTo = t, this.reset();
}
// TODO: Implement using of trimming and attention simultaneously
/**
* Resets manipulator to default state.
* @param lazy If true, the reset will be requested but not performed immediately.
* This is useful for performance optimization when multiple manipulations are performed in a short time.
* The reset will be performed on the next resize event.
* If false, the reset will be performed immediately.
*/
reset(t = !1) {
var e;
if (t) {
this.resetRequested = !0;
return;
}
if (this._trimTo)
this.showRegion(this._trimTo, !0);
else if ((e = this._attention) != null && e[0])
this.showRegion(
// TODO: Implement using of multiple attentions
I(
this._attention[0],
this._image,
{ width: this.canvasWidth, height: this.canvasHeight },
!1
)
);
else {
const o = H(this._state.angle), s = Math.abs(Math.cos(o)), i = Math.abs(Math.sin(o)), a = this.fitWidth * s + this.fitHeight * i, c = this.fitHeight * s + this.fitWidth * i;
this.manipulate({
scale: W(
{ width: a, height: c },
{ width: this.canvasWidth, height: this.canvasHeight }
),
offsetX: 0,
offsetY: 0
});
}
}
manipulate(t) {
if (!this._ctx || Object.values(t).every((s) => s === void 0)) return;
let e = !1;
if (t.angle !== void 0 || t.dAngle !== void 0) {
const s = t.angle ?? this._state.angle + (t.dAngle ?? 0), i = V(s);
this._state.angle !== i && (this._state.angle = i, e = !0);
}
const o = this._state.scale;
if (e || t.scale !== void 0 || t.dScale !== void 0 || t.offsetX !== void 0 || t.dOffsetX !== void 0 || t.offsetY !== void 0 || t.dOffsetY !== void 0) {
const s = H(this._state.angle), i = Math.abs(Math.cos(s)), a = Math.abs(Math.sin(s)), c = this.fitWidth * i + this.fitHeight * a, r = this.fitHeight * i + this.fitWidth * a;
let g = !1;
if (e || t.scale !== void 0 || t.dScale !== void 0) {
const d = Math.min(
this.canvasWidth / c,
this.canvasHeight / r
), f = t.scale ?? this._state.scale * (t.dScale ?? 1), l = Math.max(d, f);
this._state.scale !== l && (this._state.scale = l, g = !0, e = !0);
}
if (g) {
const d = this._state.scale / o, f = t.centerX ?? this.canvasWidth / 2, l = t.centerY ?? this.canvasHeight / 2, x = this.canvasWidth / 2 + this._state.offsetX * this.canvasWidth, w = this.canvasHeight / 2 + this._state.offsetY * this.canvasHeight, v = f - x, _ = l - w;
this._state.offsetX += (v - v * d) / this.canvasWidth, this._state.offsetY += (_ - _ * d) / this.canvasHeight;
}
if (e || t.offsetX !== void 0 || t.dOffsetX !== void 0) {
const d = t.offsetX ?? this._state.offsetX + (t.dOffsetX ?? 0) / this.clientWidth, f = Math.abs(this.canvasWidth - c * this._state.scale) / 2 / this.canvasWidth, l = Math.max(-f, Math.min(d, f));
this._state.offsetX !== l && (this._state.offsetX = l, e = !0);
}
if (e || t.offsetY !== void 0 || t.dOffsetY !== void 0) {
const d = t.offsetY ?? this._state.offsetY + (t.dOffsetY ?? 0) / this.clientHeight, f = Math.abs(this.canvasHeight - r * this._state.scale) / 2 / this.canvasHeight, l = Math.max(-f, Math.min(d, f));
this._state.offsetY !== l && (this._state.offsetY = l, e = !0);
}
}
e && this.redraw();
}
showRegion(t, e = !1) {
const o = p(this._image, {
width: this.canvasWidth,
height: this.canvasHeight
}), s = (e ? W : p)(t, {
width: this.canvasWidth,
height: this.canvasHeight
}), i = t.x + t.width / 2, a = t.y + t.height / 2, c = (this._image.width / 2 - i) * s / this.canvasWidth, r = (this._image.height / 2 - a) * s / this.canvasHeight;
this.manipulate({
scale: s / o,
offsetX: c,
offsetY: r
});
}
getCurrentRegion() {
const t = this._state.scale, e = p(this._image, {
width: this.canvasWidth,
height: this.canvasHeight
});
return {
x: (this._image.width * t * e - this.canvasWidth - 2 * this._state.offsetX * this.canvasWidth) / (t * e * 2),
y: (this._image.height * t * e - this.canvasHeight - 2 * this._state.offsetY * this.canvasHeight) / (t * e * 2),
width: this.canvasWidth / t / e,
height: this.canvasHeight / t / e
};
}
close() {
this.resizeObserver.disconnect();
}
redraw() {
if (!this._ctx) return;
const t = this._state.scale, e = this.fitWidth * t, o = this.fitHeight * t;
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) {
const s = this.canvasWidth / 2, i = this.canvasHeight / 2;
this._ctx.translate(s, i), this._ctx.rotate(H(this._state.angle)), this._ctx.translate(-s, -i);
}
this._ctx.drawImage(
this._image,
(this.canvasWidth - e) / 2,
(this.canvasHeight - o) / 2,
e,
o
), 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;
const t = p(this._image, {
width: this.canvasWidth,
height: this.canvasHeight
});
this.fitWidth = this._image.width * t, this.fitHeight = this._image.height * t;
}
}
class B {
constructor(t) {
u(this, "manipulating", !1);
u(this, "previousClick", null);
u(this, "previousTouch", null);
this.manipulator = t;
}
wheel(t) {
this.manipulator.manipulate({
dScale: Math.sign(t.deltaY) * -0.1 + 1,
centerX: t.offsetX * window.devicePixelRatio,
centerY: t.offsetY * window.devicePixelRatio
});
}
dblclick() {
this.manipulator.reset();
}
mouseMoving(t) {
if (this.manipulating && this.previousClick) {
const e = t.clientX - this.previousClick.x, o = t.clientY - this.previousClick.y;
this.previousClick = t, this.manipulator.manipulate({
dOffsetX: e,
dOffsetY: o
});
}
}
mouseDown(t) {
this.manipulating = !0, this.previousClick = t;
}
mouseUp() {
this.manipulating = !1;
}
touchMoving(t) {
var e, o, s;
if (this.manipulating) {
if ((e = t.touches) != null && e[0] && ((s = (o = this.previousTouch) == null ? void 0 : o.touches) != null && s[0]) && t.touches.length === 1) {
const i = t.touches[0].clientX - this.previousTouch.touches[0].clientX, a = t.touches[0].clientY - this.previousTouch.touches[0].clientY;
this.manipulator.manipulate({
dOffsetX: i,
dOffsetY: a
});
} else if (t.touches && t.touches.length >= 2 && this.previousTouch && this.previousTouch.touches.length >= 2) {
const i = y(this.previousTouch), a = y(t), c = t.target.getBoundingClientRect(), r = a.x - c.x, g = a.y - c.y;
this.manipulator.manipulate({
dScale: R(t) / R(this.previousTouch),
dOffsetX: a.x - i.x,
dOffsetY: a.y - i.y,
centerX: r * window.devicePixelRatio,
centerY: g * window.devicePixelRatio
});
}
t.touches && (this.previousTouch = t), t.preventDefault();
}
}
touchDown(t) {
t.touches && (this.manipulating = !0, this.previousTouch = t);
}
touchUp() {
this.manipulating = !1;
}
}
const j = {
ref: "canvasRef",
class: "fit"
}, N = {
key: 0,
class: "grid-overlay"
}, G = /* @__PURE__ */ C({
__name: "vue-pincher",
props: /* @__PURE__ */ k({
image: {},
readonly: { type: Boolean, default: !1 },
showGrid: { type: Boolean, default: !1 },
trimTo: { default: () => {
} },
attention: { default: () => [] }
}, {
modelValue: {
default: { angle: 0, offsetX: 0, offsetY: 0, scale: 0 }
},
modelModifiers: {}
}),
emits: ["update:modelValue"],
setup(h, { expose: t }) {
t({
rotate: c,
rotateTo: r,
reset: a,
showRegion: g
});
const e = S(h, "modelValue"), o = D("canvasRef");
let s, i;
E(() => {
if (!o.value) return;
const n = o.value.getContext("2d");
n && (s = new q(
n,
h.image,
e.value,
h.trimTo,
h.attention
), h.readonly || (i = new B(s)));
}), Y(() => {
s == null || s.close();
}), m(
() => h.image,
(n) => s.image = n
), m(
() => h.trimTo,
(n) => s.trimTo = n
), m(
() => h.attention,
(n) => s.attention = n
), m(e, (n) => s.state = n), m(
() => [
e.value.angle,
e.value.offsetX,
e.value.offsetY,
e.value.scale
],
() => s.redraw()
);
function a(n = !1) {
s.reset(n);
}
function c(n) {
s.manipulate({
dAngle: n
});
}
function r(n) {
s.manipulate({
angle: n
});
}
function g(n) {
s.showRegion(n);
}
h.readonly || (window.addEventListener("touchmove", f), window.addEventListener("touchend", l), window.addEventListener("mousemove", w), window.addEventListener("mouseup", v), Y(() => {
window.removeEventListener("touchmove", f), window.removeEventListener("touchend", l), window.removeEventListener("mousemove", w), window.removeEventListener("mouseup", v);
}));
function d(n) {
i == null || i.touchDown(n);
}
function f(n) {
i == null || i.touchMoving(n);
}
function l() {
i == null || i.touchUp();
}
function x(n) {
i == null || i.mouseDown(n);
}
function w(n) {
i == null || i.mouseMoving(n);
}
function v() {
i == null || i.mouseUp();
}
function _(n) {
i == null || i.wheel(n);
}
function T() {
i == null || i.dblclick();
}
return (n, J) => (M(), X("div", {
class: "fit",
style: { position: "relative" },
onMousedown: x,
onTouchstart: d,
onWheel: _,
onDblclick: T
}, [
P("canvas", j, null, 512),
z(L, null, {
default: U(() => [
n.showGrid ? (M(), X("div", N)) : A("", !0)
]),
_: 1
})
], 32));
}
}), F = (h, t) => {
const e = h.__vccOpts || h;
for (const [o, s] of t)
e[o] = s;
return e;
}, Z = /* @__PURE__ */ F(G, [["__scopeId", "data-v-5f0c7ad8"]]);
export {
Z as default
};