From 4ba18690d7abd7eea086bb59e6cbcc8ead9e121a Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Thu, 6 Nov 2025 20:25:17 +0900 Subject: feat(frontend): EXIFフレーム機能 (#16725) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * wip * wip * Update ImageEffector.ts * Update image-label-renderer.ts * Update image-label-renderer.ts * wip * Update image-label-renderer.ts * wip * wip * wip * wip * wip * wip * wip * Update use-uploader.ts * Update watermark.ts * wip * wu * wip * Update image-frame-renderer.ts * wip * wip * Update image-frame-renderer.ts * Create ImageCompositor.ts * Update ImageCompositor.ts * wip * wip * Update ImageEffector.ts * wip * Update use-uploader.ts * wip * wip * wip * wip * Update fxs.ts * wip * wip * wip * Update CHANGELOG.md * wip * wip * Update MkImageEffectorDialog.vue * Update MkImageEffectorDialog.vue * Update MkImageFrameEditorDialog.vue * Update use-uploader.ts * improve error handling * Update use-uploader.ts * 🎨 * wip * wip * lazy load * lazy load * wip * wip * wip --- .../src/components/MkImageEffectorDialog.vue | 87 +++++++++++++--------- 1 file changed, 52 insertions(+), 35 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 19ddb81919..3d7801f925 100644 --- a/packages/frontend/src/components/MkImageEffectorDialog.vue +++ b/packages/frontend/src/components/MkImageEffectorDialog.vue @@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
{{ i18n.ts.preview }}
@@ -64,6 +64,7 @@ import * as os from '@/os.js'; import { deepClone } from '@/utility/clone.js'; import { FXS } from '@/utility/image-effector/fxs.js'; import { genId } from '@/utility/id.js'; +import { prefer } from '@/preferences.js'; const props = defineProps<{ image: File; @@ -94,19 +95,19 @@ const layers = reactive([]); watch(layers, async () => { if (renderer != null) { - renderer.setLayers(layers); + renderer.render(layers); } }, { deep: true }); function addEffect(ev: MouseEvent) { - os.popupMenu(FXS.map((fx) => ({ - text: fx.name, + os.popupMenu(Object.entries(FXS).map(([id, fx]) => ({ + text: fx.uiDefinition.name, action: () => { layers.push({ id: genId(), - fxId: fx.id, - params: Object.fromEntries(Object.entries(fx.params).map(([k, v]) => [k, v.default])), - }); + fxId: id as keyof typeof FXS, + params: Object.fromEntries(Object.entries(fx.uiDefinition.params).map(([k, v]) => [k, v.default])), + } as ImageEffectorLayer); }, })), ev.currentTarget ?? ev.target); } @@ -136,7 +137,7 @@ function onLayerDelete(layer: ImageEffectorLayer) { const canvasEl = useTemplateRef('canvasEl'); -let renderer: ImageEffector | null = null; +let renderer: ImageEffector | null = null; let imageBitmap: ImageBitmap | null = null; onMounted(async () => { @@ -146,30 +147,35 @@ onMounted(async () => { await nextTick(); // waitingがレンダリングされるまで待つ - imageBitmap = await window.createImageBitmap(props.image); + try { + imageBitmap = await window.createImageBitmap(props.image); - const MAX_W = 1000; - const MAX_H = 1000; - let w = imageBitmap.width; - let h = imageBitmap.height; + const MAX_W = 1000; + const MAX_H = 1000; + let w = imageBitmap.width; + let h = imageBitmap.height; - if (w > MAX_W || h > MAX_H) { - const scale = Math.min(MAX_W / w, MAX_H / h); - w *= scale; - h *= scale; - } - - renderer = new ImageEffector({ - canvas: canvasEl.value, - renderWidth: w, - renderHeight: h, - image: imageBitmap, - fxs: FXS, - }); + if (w > MAX_W || h > MAX_H) { + const scale = Math.min(MAX_W / w, MAX_H / h); + w = Math.floor(w * scale); + h = Math.floor(h * scale); + } - await renderer.setLayers(layers); + renderer = new ImageEffector({ + canvas: canvasEl.value, + renderWidth: w, + renderHeight: h, + image: imageBitmap, + }); - renderer.render(); + await renderer.render(layers); + } catch (err) { + console.error(err); + os.alert({ + type: 'error', + text: i18n.ts._imageEffector.failedToLoadImage, + }); + } closeWaiting(); }); @@ -196,7 +202,7 @@ async function save() { await nextTick(); // waitingがレンダリングされるまで待つ renderer.changeResolution(imageBitmap.width, imageBitmap.height); // 本番レンダリングのためオリジナル画質に戻す - renderer.render(); // toBlobの直前にレンダリングしないと何故か壊れる + await renderer.render(layers); // toBlobの直前にレンダリングしないと何故か壊れる canvasEl.value.toBlob((blob) => { emit('ok', new File([blob!], `image-${Date.now()}.png`, { type: 'image/png' })); dialog.value?.close(); @@ -208,11 +214,10 @@ const enabled = ref(true); watch(enabled, () => { if (renderer != null) { if (enabled.value) { - renderer.setLayers(layers); + renderer.render(layers); } else { - renderer.setLayers([]); + renderer.render([]); } - renderer.render(); } }); @@ -281,6 +286,7 @@ function onImagePointerdown(ev: PointerEvent) { angle: 0, opacity: 1, color: [1, 1, 1], + ellipse: false, }, }); } else if (penMode.value === 'blur') { @@ -294,6 +300,7 @@ function onImagePointerdown(ev: PointerEvent) { scaleY: 0.1, angle: 0, radius: 3, + ellipse: false, }, }); } else if (penMode.value === 'pixelate') { @@ -307,6 +314,7 @@ function onImagePointerdown(ev: PointerEvent) { scaleY: 0.1, angle: 0, strength: 0.2, + ellipse: false, }, }); } @@ -329,7 +337,7 @@ function onImagePointerdown(ev: PointerEvent) { const scaleY = Math.abs(y - startY); const layerIndex = layers.findIndex((l) => l.id === id); - const layer = layerIndex !== -1 ? layers[layerIndex] : null; + const layer = layerIndex !== -1 ? (layers[layerIndex] as Extract) : null; if (layer != null) { layer.params.offsetX = (x + startX) - 1; layer.params.offsetY = (y + startY) - 1; @@ -373,8 +381,17 @@ function onImagePointerdown(ev: PointerEvent) { .preview { position: relative; background-color: var(--MI_THEME-bg); - background-size: auto auto; - background-image: repeating-linear-gradient(135deg, transparent, transparent 6px, var(--MI_THEME-panel) 6px, var(--MI_THEME-panel) 12px); + background-image: linear-gradient(135deg, transparent 30%, var(--MI_THEME-panel) 30%, var(--MI_THEME-panel) 50%, transparent 50%, transparent 80%, var(--MI_THEME-panel) 80%, var(--MI_THEME-panel) 100%); + background-size: 20px 20px; +} + +.animatedBg { + animation: bg 1.2s linear infinite; +} + +@keyframes bg { + 0% { background-position: 0 0; } + 100% { background-position: -20px -20px; } } .previewContainer { -- cgit v1.2.3-freya