UNPKG

vue-pincher

Version:
396 lines (395 loc) 12.9 kB
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 };