mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-07-25 08:35:02 +02:00
wip
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
39
packages/frontend/src/utility/animation.ts
Normal file
39
packages/frontend/src/utility/animation.ts
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user