1
0
mirror of https://github.com/misskey-dev/misskey.git synced 2026-07-25 07:25:04 +02:00
This commit is contained in:
syuilo
2026-07-10 09:21:42 +09:00
parent e41aedeb06
commit 2e54afd3a7
4 changed files with 93 additions and 25 deletions

View File

@@ -78,7 +78,7 @@ const originalImageLoaded = ref(false);
const padding = 30;
const ANIMATION_DURATION = 200;
function calcDefaultSize(image: Image) {
function calcNeutralSize(image: Image) {
const maxWidth = window.innerWidth - padding * 2;
const maxHeight = window.innerHeight - padding * 2;
@@ -98,8 +98,8 @@ function calcDefaultSize(image: Image) {
return { width, height };
}
function calcDefaultTranslation(image: Image) {
const defaultSize = calcDefaultSize(image);
function calcNeutralTranslation(image: Image) {
const defaultSize = calcNeutralSize(image);
const x = (window.innerWidth - defaultSize.width) / 2;
const y = (window.innerHeight - defaultSize.height) / 2;
@@ -107,11 +107,11 @@ function calcDefaultTranslation(image: Image) {
return { x, y };
}
const defaultSize = calcDefaultSize(props.image);
const defaultTranslation = calcDefaultTranslation(props.image);
const neutralSize = calcNeutralSize(props.image);
const neutralTranslation = calcNeutralTranslation(props.image);
const size = ref({ width: defaultSize.width, height: defaultSize.height });
const translation = ref({ x: defaultTranslation.x, y: defaultTranslation.y });
const size = ref({ width: neutralSize.width, height: neutralSize.height });
const translation = ref({ x: neutralTranslation.x, y: neutralTranslation.y });
const isZooming = ref(false);
@@ -158,7 +158,7 @@ function zoomInTo(x: number, y: number, factor = 1.1, withAnimation = false) {
}
}
function resetSizeAndTranslation() {
function resetToNeutral() {
isZooming.value = false;
beginAnimation({
from: {
@@ -168,10 +168,10 @@ function resetSizeAndTranslation() {
y: translation.value.y,
},
to: {
width: defaultSize.width,
height: defaultSize.height,
x: defaultTranslation.x,
y: defaultTranslation.y,
width: neutralSize.width,
height: neutralSize.height,
x: neutralTranslation.x,
y: neutralTranslation.y,
},
duration: ANIMATION_DURATION,
easing: easing_easeInOutQuad,
@@ -195,11 +195,11 @@ function onWheel(event: WheelEvent) {
const newWidth = size.value.width * scale;
const newHeight = size.value.height * scale;
if (newWidth < defaultSize.width || newHeight < defaultSize.height) {
size.value.width = defaultSize.width;
size.value.height = defaultSize.height;
translation.value.x = defaultTranslation.x;
translation.value.y = defaultTranslation.y;
if (newWidth < neutralSize.width || newHeight < neutralSize.height) {
size.value.width = neutralSize.width;
size.value.height = neutralSize.height;
translation.value.x = neutralTranslation.x;
translation.value.y = neutralTranslation.y;
isZooming.value = false;
return;
}
@@ -212,9 +212,9 @@ function onZoomGesture(ev: { delta: number; centerX: number; centerY: number })
}
function onZoomGestureEnd() {
if (size.value.width < defaultSize.width || size.value.height < defaultSize.height) {
if (size.value.width < neutralSize.width || size.value.height < neutralSize.height) {
isZooming.value = false;
resetSizeAndTranslation();
resetToNeutral();
}
}
@@ -345,7 +345,7 @@ function onPointerup(ev: PointerEvent) {
return;
}
resetSizeAndTranslation();
resetToNeutral();
} else if (isHorizontalSwiping) {
const shouldNext = horizontalSwipeDelta < -150 || (horizontalSwipeDelta < 0 && pointerVec.x < -3); // 左の方で離された、または左に向かって強めに弾かれた
const shouldPrev = horizontalSwipeDelta > 150 || (horizontalSwipeDelta > 0 && pointerVec.x > 3); // 右の方で離された、または右に向かって強めに弾かれた
@@ -371,7 +371,7 @@ const doubleTapDetector = makeDoubleTapDetector((ev) => {
if (isZooming.value) {
isZooming.value = false;
resetSizeAndTranslation();
resetToNeutral();
} else {
isZooming.value = true;
zoomInTo(ev.touches[0].clientX, ev.touches[0].clientY, 2, true);
@@ -393,6 +393,7 @@ function onTouchmove(ev: TouchEvent) {
let rafHandle: ReturnType<typeof window['requestAnimationFrame']> | null = null;
let latestInertiaTimeStamp = 0;
//#region inertia
function updateInertia(timeStamp: number) {
rafHandle = window.requestAnimationFrame(updateInertia);
const timeDelta = timeStamp - latestInertiaTimeStamp;
@@ -414,7 +415,67 @@ onMounted(() => {
onBeforeUnmount(() => {
if (rafHandle != null) window.cancelAnimationFrame(rafHandle);
});
//#endregion
onMounted(() => {
// 画像の初期位置・サイズをsourceElementの位置に合わせてdefaultの位置にアニメーションする
if (props.image.sourceElement != null) {
const elementStyles = window.getComputedStyle(props.image.sourceElement);
const fit = elementStyles.objectFit;
const sourceRect = props.image.sourceElement.getBoundingClientRect();
if (fit === 'contain') {
const sourceAspectRatio = sourceRect.width / sourceRect.height;
const imageAspectRatio = props.image.width / props.image.height;
if (sourceAspectRatio > imageAspectRatio) {
// 横長の画像
const newWidth = sourceRect.height * imageAspectRatio;
const newHeight = sourceRect.height;
size.value.width = newWidth;
size.value.height = newHeight;
translation.value.x = sourceRect.left + (sourceRect.width - newWidth) / 2;
translation.value.y = sourceRect.top;
} else {
// 縦長の画像
const newWidth = sourceRect.width;
const newHeight = sourceRect.width / imageAspectRatio;
size.value.width = newWidth;
size.value.height = newHeight;
translation.value.x = sourceRect.left;
translation.value.y = sourceRect.top + (sourceRect.height - newHeight) / 2;
}
} else {
size.value.width = sourceRect.width;
size.value.height = sourceRect.height;
translation.value.x = sourceRect.left;
translation.value.y = sourceRect.top;
}
beginAnimation({
from: {
width: size.value.width,
height: size.value.height,
x: translation.value.x,
y: translation.value.y,
},
to: {
width: neutralSize.width,
height: neutralSize.height,
x: neutralTranslation.x,
y: neutralTranslation.y,
},
duration: 300,
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;
},
});
}
});
</script>
<style lang="scss" module>

View File

@@ -36,6 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:src="src ?? undefined"
:title="title ?? undefined"
:alt="alt ?? undefined"
:data-marker="marker ?? undefined"
loading="eager"
decoding="async"
draggable="false"
@@ -87,8 +88,8 @@ const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resol
<script lang="ts" setup>
import { computed, nextTick, onMounted, onUnmounted, useTemplateRef, watch, ref } from 'vue';
import { genId } from '@/utility/id.js';
import { render } from 'buraha';
import { genId } from '@/utility/id.js';
import { prefer } from '@/preferences.js';
const props = withDefaults(defineProps<{
@@ -110,6 +111,7 @@ const props = withDefaults(defineProps<{
cover?: boolean;
forceBlurhash?: boolean;
onlyAvgColor?: boolean; // 軽量化のためにBlurhashを使わずに平均色だけを描画
marker?: string;
}>(), {
transition: null,
src: null,

View File

@@ -29,6 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:height="image.properties.height"
:style="hide ? 'filter: brightness(0.7);' : null"
:class="$style.image"
:marker="marker"
/>
<div
v-else-if="prefer.s.dataSaver.media || hide"
@@ -42,6 +43,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:alt="image.comment || image.name"
:title="image.comment || image.name"
:class="$style.image"
:data-marker="marker"
/>
</component>
<template v-if="hide">
@@ -85,6 +87,7 @@ const props = withDefaults(defineProps<{
cover?: boolean;
disableImageLink?: boolean;
controls?: boolean;
marker?: string;
}>(), {
cover: false,
disableImageLink: false,

View File

@@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
>
<template v-for="media in mediaList.filter(media => previewable(media))">
<XVideo v-if="media.type.startsWith('video')" :key="`video:${media.id}`" :class="$style.media" :video="media"/>
<XImage v-else-if="media.type.startsWith('image')" :key="`image:${media.id}`" :disableImageLink="true" :class="$style.media" :data-id="media.id" :image="media" :raw="raw" @click="openGallery(media.id)"/>
<XImage v-else-if="media.type.startsWith('image')" :key="`image:${media.id}`" :marker="`${markerId}:${media.id}`" :disableImageLink="true" :class="$style.media" :image="media" :raw="raw" @click="openGallery(media.id)"/>
</template>
</div>
</div>
@@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, onMounted, onUnmounted, useTemplateRef } from 'vue';
import { computed, markRaw, onMounted, onUnmounted, useTemplateRef } from 'vue';
import * as Misskey from 'misskey-js';
import { FILE_TYPE_BROWSERSAFE } from '@@/js/const.js';
import XBanner from '@/components/MkMediaBanner.vue';
@@ -38,6 +38,7 @@ import XVideo from '@/components/MkMediaVideo.vue';
import * as os from '@/os.js';
import { focusParent } from '@/utility/focus.js';
import { prefer } from '@/preferences.js';
import { genId } from '@/utility/id.js';
const props = defineProps<{
mediaList: Misskey.entities.DriveFile[];
@@ -46,6 +47,7 @@ const props = defineProps<{
const gallery = useTemplateRef('gallery');
const count = computed(() => props.mediaList.filter(media => previewable(media)).length);
const markerId = genId();
async function calcAspectRatio() {
if (!gallery.value) return;
@@ -100,7 +102,7 @@ async function openGallery(id: string) {
thumbnailUrl: media.thumbnailUrl,
width: media.properties.width ?? 0,
height: media.properties.height ?? 0,
sourceElement: gallery.value?.querySelector(`.image[data-id="${media.id}"]`) as HTMLElement | undefined,
sourceElement: markRaw(gallery.value?.querySelector(`[data-marker="${markerId}:${media.id}"]`)),
}));
const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkImageGallery.vue').then(x => x.default), {
defaultIndex: images.findIndex(image => image.id === id),