UNPKG

vue-pincher

Version:
400 lines (399 loc) 13.5 kB
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 };