From 8c413d01e64d0fc307d1ea9c36eca87701531d37 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Wed, 17 Sep 2025 18:38:56 +0900
Subject: enhance(frontend): マスクエフェクト (#16556)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* wip

* wip

* Update MkImageEffectorDialog.vue

* Update MkImageEffectorDialog.vue

* Update MkImageEffectorDialog.vue

* Update MkImageEffectorDialog.vue

* Update MkImageEffectorDialog.vue

* Update fillSquare.ts

* Update CHANGELOG.md

* Update fillSquare.ts
---
 .../src/components/MkImageEffectorDialog.vue       | 119 ++++++++++++++++++++-
 1 file changed, 115 insertions(+), 4 deletions(-)

(limited to 'packages/frontend/src/components/MkImageEffectorDialog.vue')

diff --git a/packages/frontend/src/components/MkImageEffectorDialog.vue b/packages/frontend/src/components/MkImageEffectorDialog.vue
index 2c6185fd33..f794af290a 100644
--- a/packages/frontend/src/components/MkImageEffectorDialog.vue
+++ b/packages/frontend/src/components/MkImageEffectorDialog.vue
@@ -19,9 +19,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<div :class="$style.root">
 		<div :class="$style.container">
 			<div :class="$style.preview">
-				<canvas ref="canvasEl" :class="$style.previewCanvas"></canvas>
+				<canvas ref="canvasEl" :class="$style.previewCanvas" @pointerdown="onImagePointerdown"></canvas>
 				<div :class="$style.previewContainer">
 					<div class="_acrylic" :class="$style.previewTitle">{{ i18n.ts.preview }}</div>
+					<div class="_acrylic" :class="$style.editControls">
+						<button class="_button" :class="[$style.previewControlsButton, fillSquare ? $style.active : null]" @click="fillSquare = true"><i class="ti ti-pencil"></i></button>
+					</div>
 					<div class="_acrylic" :class="$style.previewControls">
 						<button class="_button" :class="[$style.previewControlsButton, !enabled ? $style.active : null]" @click="enabled = false">Before</button>
 						<button class="_button" :class="[$style.previewControlsButton, enabled ? $style.active : null]" @click="enabled = true">After</button>
@@ -212,6 +215,100 @@ watch(enabled, () => {
 		renderer.render();
 	}
 });
+
+const fillSquare = ref(false);
+
+function onImagePointerdown(ev: PointerEvent) {
+	if (canvasEl.value == null || imageBitmap == null || !fillSquare.value) return;
+
+	const AW = canvasEl.value.clientWidth;
+	const AH = canvasEl.value.clientHeight;
+	const BW = imageBitmap.width;
+	const BH = imageBitmap.height;
+
+	let xOffset = 0;
+	let yOffset = 0;
+
+	if (AW / AH < BW / BH) { // 横長
+		yOffset = AH - BH * (AW / BW);
+	} else { // 縦長
+		xOffset = AW - BW * (AH / BH);
+	}
+
+	xOffset /= 2;
+	yOffset /= 2;
+
+	let startX = ev.offsetX - xOffset;
+	let startY = ev.offsetY - yOffset;
+
+	if (AW / AH < BW / BH) { // 横長
+		startX = startX / (Math.max(AW, AH) / Math.max(BH / BW, 1));
+		startY = startY / (Math.max(AW, AH) / Math.max(BW / BH, 1));
+	} else { // 縦長
+		startX = startX / (Math.min(AW, AH) / Math.max(BH / BW, 1));
+		startY = startY / (Math.min(AW, AH) / Math.max(BW / BH, 1));
+	}
+
+	const id = genId();
+	layers.push({
+		id,
+		fxId: 'fillSquare',
+		params: {
+			offsetX: 0,
+			offsetY: 0,
+			scaleX: 0.1,
+			scaleY: 0.1,
+			angle: 0,
+			opacity: 1,
+			color: [1, 1, 1],
+		},
+	});
+
+	_move(ev.offsetX, ev.offsetY);
+
+	function _move(pointerX: number, pointerY: number) {
+		let x = pointerX - xOffset;
+		let y = pointerY - yOffset;
+
+		if (AW / AH < BW / BH) { // 横長
+			x = x / (Math.max(AW, AH) / Math.max(BH / BW, 1));
+			y = y / (Math.max(AW, AH) / Math.max(BW / BH, 1));
+		} else { // 縦長
+			x = x / (Math.min(AW, AH) / Math.max(BH / BW, 1));
+			y = y / (Math.min(AW, AH) / Math.max(BW / BH, 1));
+		}
+
+		const scaleX = Math.abs(x - startX);
+		const scaleY = Math.abs(y - startY);
+
+		const layerIndex = layers.findIndex((l) => l.id === id);
+		const layer = layerIndex !== -1 ? layers[layerIndex] : null;
+		if (layer != null) {
+			layer.params.offsetX = (x + startX) - 1;
+			layer.params.offsetY = (y + startY) - 1;
+			layer.params.scaleX = scaleX;
+			layer.params.scaleY = scaleY;
+			layers[layerIndex] = layer;
+		}
+	}
+
+	function move(ev: PointerEvent) {
+		_move(ev.offsetX, ev.offsetY);
+	}
+
+	function up() {
+		canvasEl.value?.removeEventListener('pointermove', move);
+		canvasEl.value?.removeEventListener('pointerup', up);
+		canvasEl.value?.removeEventListener('pointercancel', up);
+		canvasEl.value?.releasePointerCapture(ev.pointerId);
+
+		fillSquare.value = false;
+	}
+
+	canvasEl.value.addEventListener('pointermove', move);
+	canvasEl.value.addEventListener('pointerup', up);
+	canvasEl.value.setPointerCapture(ev.pointerId);
+}
 </script>
 
 <style module>
@@ -251,6 +348,18 @@ watch(enabled, () => {
 	font-size: 85%;
 }
 
+.editControls {
+	position: absolute;
+	z-index: 100;
+	bottom: 8px;
+	left: 8px;
+	display: flex;
+	align-items: center;
+	gap: 8px;
+	padding: 6px 10px;
+	border-radius: 6px;
+}
+
 .previewControls {
 	position: absolute;
 	z-index: 100;
@@ -283,9 +392,11 @@ watch(enabled, () => {
 	position: absolute;
 	top: 0;
 	left: 0;
-	width: 100%;
-	height: 100%;
-	padding: 20px;
+	width: -webkit-fill-available;
+	width: stretch;
+	height: -webkit-fill-available;
+	height: stretch;
+	margin: 20px;
 	box-sizing: border-box;
 	object-fit: contain;
 }
-- 
cgit v1.2.3-freya

