mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-07-25 07:25:04 +02:00
wip
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user