From 53b4d184ace5c76d4bd07ce2b35b5ad23f57b345 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Fri, 10 Jul 2026 14:16:38 +0900 Subject: [PATCH] refactor --- .../src/components/MkImageGallery.item.vue | 149 ++++++++---------- .../src/components/MkImageGallery.utils.ts | 16 +- 2 files changed, 71 insertions(+), 94 deletions(-) diff --git a/packages/frontend/src/components/MkImageGallery.item.vue b/packages/frontend/src/components/MkImageGallery.item.vue index 9351489ec1..f40bf9ae83 100644 --- a/packages/frontend/src/components/MkImageGallery.item.vue +++ b/packages/frontend/src/components/MkImageGallery.item.vue @@ -14,24 +14,28 @@ SPDX-License-Identifier: AGPL-3.0-only @touchmove.passive="onTouchmove" @wheel="onWheel" > -
+
@@ -85,7 +89,7 @@ const padding = 30; const ANIMATION_DURATION = 200; // maxからはみ出す場合は縮小、maxに満たない場合は拡大する(contain) -function calcNeutralSize(image: Image) { +function calcImageRenderingSize(image: Image) { const maxWidth = window.innerWidth - padding * 2; const maxHeight = window.innerHeight - padding * 2; @@ -99,74 +103,62 @@ function calcNeutralSize(image: Image) { return { width, height }; } -function calcNeutralTranslation(image: Image) { - return { x: 0, y: 0 }; +const imageRenderingSize = calcImageRenderingSize(props.image); +const transform = ref({ x: 0, y: 0, scale: 1 }); + +// 元のimg要素の位置・サイズ(とobject-fitの設定値)を取得して、そこからneutralの位置にアニメーションするためのscaleとtranslationを計算する +function getScaleAndTranslationForSourceElement(): { x: number; y: number; scale: number } { + const sourceElement = props.image.sourceElement; + if (sourceElement == null) return { x: 0, y: 0, scale: 1 }; + + return calculateSourceTransform({ + fit: window.getComputedStyle(sourceElement).objectFit, + imageRenderingSize, + sourceRect: sourceElement.getBoundingClientRect(), + viewportSize: { + width: window.innerWidth, + height: window.innerHeight, + }, + }); } -const neutralSize = calcNeutralSize(props.image); -const neutralTranslation = calcNeutralTranslation(props.image); - -const size = { width: neutralSize.width, height: neutralSize.height }; -const translation = ref({ x: neutralTranslation.x, y: neutralTranslation.y }); -const transformScale = ref(1); - if (props.image.sourceElement != null && props.activated) { const sourceTransform = getScaleAndTranslationForSourceElement(); - translation.value.x = sourceTransform.x; - translation.value.y = sourceTransform.y; - transformScale.value = sourceTransform.scale; + transform.value.scale = sourceTransform.scale; + transform.value.x = sourceTransform.x; + transform.value.y = sourceTransform.y; } const isZooming = ref(false); function zoomInTo(x: number, y: number, factor = 1.1, withAnimation = false) { - const newScale = transformScale.value * factor; + const newScale = transform.value.scale * factor; isZooming.value = true; const rect = rootEl.value.getBoundingClientRect(); const offsetX = x - rect.left; const offsetY = y - rect.top; - const newTranslationX = offsetX - (offsetX - translation.value.x) * factor; - const newTranslationY = offsetY - (offsetY - translation.value.y) * factor; + const newTranslationX = offsetX - (offsetX - transform.value.x) * factor; + const newTranslationY = offsetY - (offsetY - transform.value.y) * factor; if (withAnimation) { - beginAnimation({ - from: { - x: translation.value.x, - y: translation.value.y, - scale: transformScale.value, - }, - to: { - x: newTranslationX, - y: newTranslationY, - scale: newScale, - }, - duration: ANIMATION_DURATION, - easing: easing_easeInOutQuad, - apply: (state) => { - translation.value.x = state.x; - translation.value.y = state.y; - transformScale.value = state.scale; - }, - }); - } else { - translation.value.x = newTranslationX; - translation.value.y = newTranslationY; - transformScale.value = newScale; + enableTransition.value = true; } + + transform.value.x = newTranslationX; + transform.value.y = newTranslationY; + transform.value.scale = newScale; } function resetToNeutral() { isZooming.value = false; enableTransition.value = true; - rootEl.value.offsetHeight; // reflow - - translation.value.x = neutralTranslation.x; - translation.value.y = neutralTranslation.y; - transformScale.value = 1; + transform.value.scale = 1; + transform.value.x = 0; + transform.value.y = 0; } function onWheel(event: WheelEvent) { @@ -177,12 +169,12 @@ function onWheel(event: WheelEvent) { const scaleFactor = 1.1; const scale = delta > 0 ? 1 / scaleFactor : scaleFactor; - const newScale = transformScale.value * scale; + const newScale = transform.value.scale * scale; if (newScale < 1) { - translation.value.x = neutralTranslation.x; - translation.value.y = neutralTranslation.y; - transformScale.value = 1; + transform.value.scale = 1; + transform.value.x = 0; + transform.value.y = 0; isZooming.value = false; return; } @@ -195,7 +187,7 @@ function onZoomGesture(ev: { delta: number; centerX: number; centerY: number }) } function onZoomGestureEnd() { - if (transformScale.value < 1) { + if (transform.value.scale < 1) { isZooming.value = false; resetToNeutral(); } @@ -269,14 +261,13 @@ function onPointermove(ev: PointerEvent) { const deltaY = ev.clientY - lastY; if (isZooming.value) { - translation.value.x += deltaX; - translation.value.y += deltaY; + transform.value.x += deltaX; + transform.value.y += deltaY; } else { if (isVerticalSwiping) { - translation.value.y += deltaY; + transform.value.y += deltaY; verticalSwipeDelta += deltaY; } else if (isHorizontalSwiping) { - //translation.value.x += deltaX; horizontalSwipeDelta = ev.clientX - currentPointerStartOffset.x; emit('horizontalSwipe', horizontalSwipeDelta); } else { @@ -317,12 +308,10 @@ function onPointerup(ev: PointerEvent) { const sourceTransform = getScaleAndTranslationForSourceElement(); enableTransition.value = true; - rootEl.value.offsetHeight; // reflow - - translation.value.x = sourceTransform.x; - translation.value.y = sourceTransform.y; - transformScale.value = sourceTransform.scale; + transform.value.x = sourceTransform.x; + transform.value.y = sourceTransform.y; + transform.value.scale = sourceTransform.scale; return; } @@ -383,8 +372,8 @@ function updateInertia(timeStamp: number) { if (isDragging) return; if (!isZooming.value) return; if (Math.abs(pointerVec.x) < 0.01 && Math.abs(pointerVec.y) < 0.01) return; - translation.value.x += pointerVec.x * timeDelta; - translation.value.y += pointerVec.y * timeDelta; + transform.value.x += pointerVec.x * timeDelta; + transform.value.y += pointerVec.y * timeDelta; pointerVec.x *= inertiaFactor ** (timeDelta / 16.67); pointerVec.y *= inertiaFactor ** (timeDelta / 16.67); } @@ -398,31 +387,13 @@ onBeforeUnmount(() => { }); //#endregion -// 元のimg要素の位置・サイズ(とobject-fitの設定値)を取得して、そこからneutralの位置にアニメーションするためのscaleとtranslationを計算する -function getScaleAndTranslationForSourceElement(): { x: number; y: number; scale: number } { - const sourceElement = props.image.sourceElement; - if (sourceElement == null) return { x: 0, y: 0, scale: 1 }; - - return calculateSourceTransform({ - fit: window.getComputedStyle(sourceElement).objectFit, - neutralSize, - sourceRect: sourceElement.getBoundingClientRect(), - viewportSize: { - width: window.innerWidth, - height: window.innerHeight, - }, - }); -} - onMounted(async () => { if (props.image.sourceElement != null && props.activated) { enableTransition.value = true; - rootEl.value.offsetHeight; // reflow - - translation.value.x = neutralTranslation.x; - translation.value.y = neutralTranslation.y; - transformScale.value = 1; + transform.value.x = 0; + transform.value.y = 0; + transform.value.scale = 1; } }); @@ -459,6 +430,12 @@ onMounted(async () => { pointer-events: none; } +.transformer { + width: 100%; + height: 100%; + transform-origin: left top; +} + .transition { transition: translate 200ms ease, scale 200ms ease; } diff --git a/packages/frontend/src/components/MkImageGallery.utils.ts b/packages/frontend/src/components/MkImageGallery.utils.ts index 001fbc6085..0847e79ae5 100644 --- a/packages/frontend/src/components/MkImageGallery.utils.ts +++ b/packages/frontend/src/components/MkImageGallery.utils.ts @@ -15,23 +15,23 @@ type Rect = Size & { export function calculateSourceTransform({ fit, - neutralSize, + imageRenderingSize, sourceRect, viewportSize, }: { fit: string; - neutralSize: Size; + imageRenderingSize: Size; sourceRect: Rect; viewportSize: Size; }): { x: number; y: number; scale: number } { const scale = fit === 'cover' - ? Math.max(sourceRect.width / neutralSize.width, sourceRect.height / neutralSize.height) - : Math.min(sourceRect.width / neutralSize.width, sourceRect.height / neutralSize.height); + ? Math.max(sourceRect.width / imageRenderingSize.width, sourceRect.height / imageRenderingSize.height) + : Math.min(sourceRect.width / imageRenderingSize.width, sourceRect.height / imageRenderingSize.height); - const neutralLeft = (viewportSize.width - neutralSize.width) / 2; - const neutralTop = (viewportSize.height - neutralSize.height) / 2; - const sourceImageWidth = neutralSize.width * scale; - const sourceImageHeight = neutralSize.height * scale; + const neutralLeft = (viewportSize.width - imageRenderingSize.width) / 2; + const neutralTop = (viewportSize.height - imageRenderingSize.height) / 2; + const sourceImageWidth = imageRenderingSize.width * scale; + const sourceImageHeight = imageRenderingSize.height * scale; const sourceImageLeft = sourceRect.left + (sourceRect.width - sourceImageWidth) / 2; const sourceImageTop = sourceRect.top + (sourceRect.height - sourceImageHeight) / 2;