diff --git a/packages/frontend/src/components/MkImageGallery.item.vue b/packages/frontend/src/components/MkImageGallery.item.vue index 741968a7ae..2a34795f04 100644 --- a/packages/frontend/src/components/MkImageGallery.item.vue +++ b/packages/frontend/src/components/MkImageGallery.item.vue @@ -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); diff --git a/packages/frontend/src/components/MkImageGallery.vue b/packages/frontend/src/components/MkImageGallery.vue index 29b67d9c3e..afc6b2beef 100644 --- a/packages/frontend/src/components/MkImageGallery.vue +++ b/packages/frontend/src/components/MkImageGallery.vue @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
(); +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 { diff --git a/packages/frontend/src/utility/animation.ts b/packages/frontend/src/utility/animation.ts new file mode 100644 index 0000000000..28984ffdd1 --- /dev/null +++ b/packages/frontend/src/utility/animation.ts @@ -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>(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); +}