1
0
mirror of https://github.com/misskey-dev/misskey.git synced 2026-07-25 08:35:02 +02:00
This commit is contained in:
syuilo
2026-07-09 21:05:14 +09:00
parent cc356cf542
commit 951e22242d
3 changed files with 118 additions and 77 deletions

View File

@@ -32,6 +32,7 @@ import { nextTick, onBeforeUnmount, onMounted, onUnmounted, ref, useTemplateRef
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { makeDoubleTapDetector } from '@/utility/double-tap.js';
import { beginAnimation, easing_easeInOutQuad } from '@/utility/animation.js';
type Image = {
id: string;
@@ -95,36 +96,6 @@ const defaultTranslation = calcDefaultTranslation(props.image);
const size = ref({ width: defaultSize.width, height: defaultSize.height });
const translation = ref({ x: defaultTranslation.x, y: defaultTranslation.y });
let isAnimating = false;
function beginAnimation(to: { width: number; height: number; x: number; y: number }, duration: number) {
const easing = (t: number) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; // easeInOutQuad
const startTime = performance.now();
const startSize = { ...size.value };
const startTranslation = { ...translation.value };
isAnimating = true;
function animate() {
const now = performance.now();
const elapsed = now - startTime;
const t = easing(Math.min(elapsed / duration, 1));
size.value.width = startSize.width + (to.width - startSize.width) * t;
size.value.height = startSize.height + (to.height - startSize.height) * t;
translation.value.x = startTranslation.x + (to.x - startTranslation.x) * t;
translation.value.y = startTranslation.y + (to.y - startTranslation.y) * t;
if (t < 1) {
window.requestAnimationFrame(animate);
} else {
isAnimating = false;
}
}
window.requestAnimationFrame(animate);
}
const isZooming = ref(false);
function zoomInTo(x: number, y: number, factor = 1.1, withAnimation = false) {
@@ -141,11 +112,27 @@ function zoomInTo(x: number, y: number, factor = 1.1, withAnimation = false) {
if (withAnimation) {
beginAnimation({
width: newWidth,
height: newHeight,
x: translation.value.x - offsetX * (factor - 1),
y: translation.value.y - offsetY * (factor - 1),
}, ANIMATION_DURATION);
from: {
width: size.value.width,
height: size.value.height,
x: translation.value.x,
y: translation.value.y,
},
to: {
width: newWidth,
height: newHeight,
x: translation.value.x - offsetX * (factor - 1),
y: translation.value.y - offsetY * (factor - 1),
},
duration: ANIMATION_DURATION,
easing: easing_easeInOutQuad,
apply: (state) => {
size.value.width = state.width;
size.value.height = state.height;
translation.value.x = state.x;
translation.value.y = state.y;
},
});
} else {
size.value.width = newWidth;
size.value.height = newHeight;
@@ -154,6 +141,32 @@ function zoomInTo(x: number, y: number, factor = 1.1, withAnimation = false) {
}
}
function resetSizeAndTranslation() {
isZooming.value = false;
beginAnimation({
from: {
width: size.value.width,
height: size.value.height,
x: translation.value.x,
y: translation.value.y,
},
to: {
width: defaultSize.width,
height: defaultSize.height,
x: defaultTranslation.x,
y: defaultTranslation.y,
},
duration: ANIMATION_DURATION,
easing: easing_easeInOutQuad,
apply: (state) => {
size.value.width = state.width;
size.value.height = state.height;
translation.value.x = state.x;
translation.value.y = state.y;
},
});
}
function onWheel(event: WheelEvent) {
event.preventDefault();
@@ -184,12 +197,7 @@ function onZoomGesture(ev: { delta: number; centerX: number; centerY: number })
function onZoomGestureEnd() {
if (size.value.width < defaultSize.width || size.value.height < defaultSize.height) {
isZooming.value = false;
beginAnimation({
width: defaultSize.width,
height: defaultSize.height,
x: defaultTranslation.x,
y: defaultTranslation.y,
}, ANIMATION_DURATION);
resetSizeAndTranslation();
}
}
@@ -304,12 +312,7 @@ function onPointerup(ev: PointerEvent) {
return;
}
beginAnimation({
width: defaultSize.width,
height: defaultSize.height,
x: defaultTranslation.x,
y: defaultTranslation.y,
}, ANIMATION_DURATION);
resetSizeAndTranslation();
} else if (isHorizontalSwiping) {
const shouldNext = horizontalSwipeDelta < -200 || (horizontalSwipeDelta < 0 && pointerVec.x < -3); // 左の方で離された、または左に向かって強めに弾かれた
const shouldPrev = horizontalSwipeDelta > 200 || (horizontalSwipeDelta > 0 && pointerVec.x > 3); // 右の方で離された、または右に向かって強めに弾かれた
@@ -335,12 +338,7 @@ const doubleTapDetector = makeDoubleTapDetector((ev) => {
if (isZooming.value) {
isZooming.value = false;
beginAnimation({
width: defaultSize.width,
height: defaultSize.height,
x: defaultTranslation.x,
y: defaultTranslation.y,
}, ANIMATION_DURATION);
resetSizeAndTranslation();
} else {
isZooming.value = true;
zoomInTo(ev.touches[0].clientX, ev.touches[0].clientY, 2, true);

View File

@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div ref="rootEl" :class="$style.root" :style="{ zIndex }">
<div :class="[$style.bg]"></div>
<div ref="mainEl" :class="$style.main">
<div ref="itemsEl" :class="$style.items">
<div ref="itemsEl" :class="$style.items" :style="{ left: `-${imagesOffset}px` }">
<div v-for="image in images" :key="image.src" ref="itemEl" :class="$style.item">
<XItem
:image="image"
@@ -28,6 +28,7 @@ import { nextTick, onMounted, onUnmounted, ref, useTemplateRef } from 'vue';
import XItem from './MkImageGallery.item.vue';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { beginAnimation, easing_easeInOutQuad } from '@/utility/animation.js';
type Image = {
id: string;
@@ -47,47 +48,49 @@ const emit = defineEmits<{
(ev: 'closed'): void;
}>();
let currentIndex = props.defaultIndex ?? 0;
const rootEl = useTemplateRef('rootEl');
const mainEl = useTemplateRef('mainEl');
const itemsEl = useTemplateRef('itemsEl');
const itemEl = useTemplateRef('itemEl');
const zIndex = os.claimZIndex('high');
let currentScrollLeft = 0;
let currentIndex = props.defaultIndex ?? 0;
const screenWidth = ref(window.innerWidth);
const imagesOffset = ref(currentIndex * window.innerWidth);
let currentScrollLeft = imagesOffset.value;
function onHorizontalSwipe(offset: number) {
itemsEl.value.scrollLeft = currentScrollLeft + offset;
imagesOffset.value = currentScrollLeft + offset;
}
function scrollToCurrentIndex() {
currentScrollLeft = currentIndex * screenWidth.value;
beginAnimation({
from: { value: imagesOffset.value },
to: { value: currentIndex * screenWidth.value },
duration: 300,
easing: easing_easeInOutQuad,
apply: ({ value }) => {
imagesOffset.value = value;
},
});
}
function onCancelHorizontalSwipe() {
itemsEl.value.scrollTo({
left: currentScrollLeft,
behavior: 'smooth',
});
scrollToCurrentIndex();
}
function onNext() {
if (currentIndex < props.images.length - 1) {
currentIndex++;
const el = itemEl.value[currentIndex];
currentScrollLeft = el.offsetLeft;
itemsEl.value.scrollTo({
left: currentScrollLeft,
behavior: 'smooth',
});
scrollToCurrentIndex();
}
}
function onPrev() {
if (currentIndex > 0) {
currentIndex--;
const el = itemEl.value[currentIndex];
currentScrollLeft = el.offsetLeft;
itemsEl.value.scrollTo({
left: currentScrollLeft,
behavior: 'smooth',
});
scrollToCurrentIndex();
}
}
@@ -120,11 +123,12 @@ function onItemClose() {
}
.items {
position: absolute;
display: flex;
width: 100dvw;
width: calc(v-bind("screenWidth + 'px'") * v-bind("images.length"));
height: 100dvh;
overflow: hidden;
scrollbar-width: none;
overflow: clip;
contain: strict;
}
.item {

View File

@@ -0,0 +1,39 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export const easing_easeInOutQuad = (t: number) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
export function beginAnimation<const T extends Record<string, number>>(options: {
from: T;
to: T;
duration: number;
easing?: (t: number) => number;
apply: (state: T) => void;
onEnd?: () => void;
}) {
const easing = options.easing ?? ((t: number) => t);
const startTime = performance.now();
function animate(now: number) {
const elapsed = now - startTime;
const t = easing(Math.min(elapsed / options.duration, 1));
const state = {} as T;
for (const _key in options.from) {
const key = _key as keyof T;
state[key] = options.from[key] + (options.to[key] - options.from[key]) * t;
}
options.apply(state);
if (t < 1) {
window.requestAnimationFrame(animate);
} else {
options.onEnd?.();
}
}
window.requestAnimationFrame(animate);
}