@bikariya/image-viewer 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bikariya/image-viewer",
3
3
  "configKey": "image-viewer",
4
- "version": "0.0.3",
4
+ "version": "0.0.5",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -1,7 +1,24 @@
1
1
  type __VLS_Props = {
2
+ /**
3
+ * 目标 `<img>` 元素
4
+ */
2
5
  target: HTMLImageElement;
6
+ /**
7
+ * 过渡动画时长 (ms)
8
+ * @default 400
9
+ */
3
10
  duration?: number;
4
- isOpening?: boolean;
11
+ /**
12
+ * 放大后占窗口比率
13
+ * @default 0.9
14
+ */
15
+ rate?: number;
16
+ /**
17
+ * 是否限制缩放比例
18
+ * @default false
19
+ */
20
+ clamp?: boolean;
21
+ open?: boolean;
5
22
  };
6
23
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
7
24
  close: () => any;
@@ -1,16 +1,22 @@
1
1
  <script setup>
2
2
  import { computed, ref, useEventListener, usePointer, useTemplateRef } from "#imports";
3
- const props = defineProps({
3
+ const { target, duration = 400, rate = 0.9, clamp } = defineProps({
4
4
  target: { type: null, required: true },
5
5
  duration: { type: Number, required: false },
6
- isOpening: { type: Boolean, required: false }
6
+ rate: { type: Number, required: false },
7
+ clamp: { type: Boolean, required: false },
8
+ open: { type: Boolean, required: false }
7
9
  });
8
10
  const emit = defineEmits(["close"]);
9
11
  const rootEl = useTemplateRef("root");
10
- const rate = 0.9;
12
+ useEventListener("keydown", (event) => {
13
+ if (event.key === "Escape") {
14
+ emit("close");
15
+ }
16
+ });
11
17
  const options = computed(() => {
12
18
  return {
13
- duration: props.duration ?? 400,
19
+ duration,
14
20
  easing: "ease",
15
21
  fill: "forwards"
16
22
  };
@@ -19,9 +25,7 @@ let startRect;
19
25
  let startCenter;
20
26
  let startDistance;
21
27
  const pointers = ref({});
22
- const fingers = computed(() => {
23
- return Object.values(pointers.value).slice(0, 2);
24
- });
28
+ const fingers = computed(() => Object.values(pointers.value).slice(0, 2));
25
29
  const center = computed(() => getCenter("current"));
26
30
  const distance = computed(() => getDistance("current"));
27
31
  function getCenter(mode) {
@@ -68,11 +72,16 @@ const { isHolding } = usePointer(rootEl, {
68
72
  const top = startRect.top + center.value.y - startCenter.y;
69
73
  const finalLeft = left - (center.value.x - left) * (rate2 - 1);
70
74
  const finalTop = top - (center.value.y - top) * (rate2 - 1);
75
+ const finalWidth = startRect.width * rate2;
76
+ const finalHeight = startRect.height * rate2;
77
+ if (isOverLimit(finalWidth, finalHeight, rate2)) {
78
+ return;
79
+ }
71
80
  rootEl.value.animate({
72
81
  left: finalLeft + "px",
73
82
  top: finalTop + "px",
74
- width: startRect.width * rate2 + "px",
75
- height: startRect.height * rate2 + "px"
83
+ width: finalWidth + "px",
84
+ height: finalHeight + "px"
76
85
  }, {
77
86
  duration: 0,
78
87
  fill: "forwards"
@@ -97,28 +106,52 @@ function onWheel(event) {
97
106
  const { left, top, width, height } = rootEl.value.getBoundingClientRect();
98
107
  const finalLeft = left - (event.clientX - left) * (rate2 - 1);
99
108
  const finalTop = top - (event.clientY - top) * (rate2 - 1);
109
+ const finalWidth = width * rate2;
110
+ const finalHeight = height * rate2;
111
+ if (isOverLimit(finalWidth, finalHeight, rate2)) {
112
+ return;
113
+ }
100
114
  rootEl.value.animate({
101
115
  left: finalLeft + "px",
102
116
  top: finalTop + "px",
103
- width: width * rate2 + "px",
104
- height: height * rate2 + "px"
117
+ width: finalWidth + "px",
118
+ height: finalHeight + "px"
105
119
  }, options.value);
106
120
  }
107
- useEventListener("keydown", (event) => {
108
- if (event.key === "Escape") {
109
- emit("close");
121
+ function onDoubleClick(event) {
122
+ const { left, top, width, height } = rootEl.value.getBoundingClientRect();
123
+ const { size: [coverWidth, coverHeight], advantageSide } = getFitSize("cover");
124
+ if (width < coverWidth && height < coverHeight) {
125
+ const [finalLeft, finalTop] = advantageSide === "height" ? [
126
+ `calc(50% - ${Math.round(coverWidth / 2)}px)`,
127
+ `${top - (event.clientY - top) * (coverHeight / height - 1)}px`
128
+ ] : [
129
+ `${left - (event.clientX - left) * (coverWidth / width - 1)}px`,
130
+ `calc(50% - ${Math.round(coverHeight / 2)}px)`
131
+ ];
132
+ rootEl.value.animate({
133
+ left: finalLeft,
134
+ top: finalTop,
135
+ width: Math.ceil(coverWidth) + "px",
136
+ height: Math.ceil(coverHeight) + "px"
137
+ }, options.value);
138
+ } else {
139
+ const { size: [containWidth, containHeight] } = getFitSize("contain");
140
+ rootEl.value.animate({
141
+ left: `calc(50% - ${Math.round(containWidth / 2)}px)`,
142
+ top: `calc(50% - ${Math.round(containHeight / 2)}px)`,
143
+ width: Math.floor(containWidth) + "px",
144
+ height: Math.floor(containHeight) + "px"
145
+ }, options.value);
110
146
  }
111
- });
147
+ }
112
148
  const onEnter = (el) => {
113
- const fixedWidth = window.innerWidth * rate;
114
- const fixedHeight = window.innerHeight * rate;
115
- const naturalRatio = props.target.naturalWidth / props.target.naturalHeight;
116
- const [finalWidth, finalHeight] = fixedWidth / fixedHeight > naturalRatio ? [fixedHeight * naturalRatio, fixedHeight] : [fixedWidth, fixedWidth / naturalRatio];
149
+ const { size: [containWidth, containHeight] } = getFitSize("contain");
117
150
  el.animate([getOriginalKeyframe(), {
118
- top: `calc(50% - ${Math.floor(finalHeight / 2)}px)`,
119
- left: `calc(50% - ${Math.floor(finalWidth / 2)}px)`,
120
- width: Math.floor(finalWidth) + "px",
121
- height: Math.floor(finalHeight) + "px",
151
+ top: `calc(50% - ${Math.round(containHeight / 2)}px)`,
152
+ left: `calc(50% - ${Math.round(containWidth / 2)}px)`,
153
+ width: Math.floor(containWidth) + "px",
154
+ height: Math.floor(containHeight) + "px",
122
155
  clipPath: "inset(0)"
123
156
  }], options.value);
124
157
  };
@@ -132,10 +165,20 @@ const onLeave = (el, done) => {
132
165
  }, getOriginalKeyframe(x, y)], options.value);
133
166
  animation.addEventListener("finish", done);
134
167
  };
168
+ function getFitSize(mode) {
169
+ const fixedWidth = window.innerWidth * rate;
170
+ const fixedHeight = window.innerHeight * rate;
171
+ const naturalRatio = target.naturalWidth / target.naturalHeight;
172
+ const advantageSide = fixedWidth / fixedHeight > naturalRatio ? "height" : "width";
173
+ return {
174
+ size: +(advantageSide === "height") ^ +(mode === "cover") ? [fixedHeight * naturalRatio, fixedHeight] : [fixedWidth, fixedWidth / naturalRatio],
175
+ advantageSide
176
+ };
177
+ }
135
178
  function getOriginalKeyframe(x = 0, y = 0) {
136
- const { left, top, width, height } = props.target.getBoundingClientRect();
137
- const { naturalWidth, naturalHeight } = props.target;
138
- const { objectPosition } = getComputedStyle(props.target);
179
+ const { left, top, width, height } = target.getBoundingClientRect();
180
+ const { naturalWidth, naturalHeight } = target;
181
+ const { objectPosition } = getComputedStyle(target);
139
182
  const [horizontal, vertical] = objectPosition.split(" ").map((pos) => Number(pos.slice(0, -1)) / 100);
140
183
  const ratio = width / height;
141
184
  const naturalRatio = naturalWidth / naturalHeight;
@@ -160,16 +203,27 @@ function getOriginalKeyframe(x = 0, y = 0) {
160
203
  clipPath: `inset(${clipTop}px ${clipRight}px ${clipBottom}px ${clipLeft}px)`
161
204
  };
162
205
  }
206
+ function isOverLimit(width, height, rate2) {
207
+ if (!clamp) {
208
+ return false;
209
+ }
210
+ if (rate2 > 1) {
211
+ return width > Math.max(window.innerWidth, target.width, target.naturalWidth) && height > Math.max(window.innerHeight, target.height, target.naturalHeight);
212
+ } else {
213
+ return width < Math.min(window.innerWidth, target.width, target.naturalWidth) && height < Math.min(window.innerHeight, target.height, target.naturalHeight);
214
+ }
215
+ }
163
216
  </script>
164
217
 
165
218
  <template>
166
219
  <transition @enter="onEnter" @leave="onLeave">
167
220
  <img
168
- v-if="isOpening"
221
+ v-if="open"
169
222
  ref="root"
170
223
  class="bikariya-image-viewer"
171
224
  :src="target.src"
172
225
  :draggable="false"
226
+ @dblclick="onDoubleClick"
173
227
  @wheel.prevent="onWheel"
174
228
  />
175
229
  </transition>
@@ -1,7 +1,24 @@
1
1
  type __VLS_Props = {
2
+ /**
3
+ * 目标 `<img>` 元素
4
+ */
2
5
  target: HTMLImageElement;
6
+ /**
7
+ * 过渡动画时长 (ms)
8
+ * @default 400
9
+ */
3
10
  duration?: number;
4
- isOpening?: boolean;
11
+ /**
12
+ * 放大后占窗口比率
13
+ * @default 0.9
14
+ */
15
+ rate?: number;
16
+ /**
17
+ * 是否限制缩放比例
18
+ * @default false
19
+ */
20
+ clamp?: boolean;
21
+ open?: boolean;
5
22
  };
6
23
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
7
24
  close: () => any;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bikariya/image-viewer",
3
3
  "type": "module",
4
- "version": "0.0.3",
4
+ "version": "0.0.5",
5
5
  "description": "Bikariya image viewer for Nuxt",
6
6
  "author": "KazariEX",
7
7
  "license": "MIT",