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