summaryrefslogtreecommitdiff
path: root/packages/frontend/src/components/MkWatermarkEditorDialog.vue
diff options
context:
space:
mode:
authorsyuilo <4439005+syuilo@users.noreply.github.com>2025-11-06 20:25:17 +0900
committerGitHub <noreply@github.com>2025-11-06 20:25:17 +0900
commit4ba18690d7abd7eea086bb59e6cbcc8ead9e121a (patch)
tree7d25ec47d8711d945b08e3903642f2e982f40048 /packages/frontend/src/components/MkWatermarkEditorDialog.vue
parentfix(frontend): improve startViewTransition handling (diff)
downloadmisskey-4ba18690d7abd7eea086bb59e6cbcc8ead9e121a.tar.gz
misskey-4ba18690d7abd7eea086bb59e6cbcc8ead9e121a.tar.bz2
misskey-4ba18690d7abd7eea086bb59e6cbcc8ead9e121a.zip
feat(frontend): EXIFフレーム機能 (#16725)
* 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
Diffstat (limited to 'packages/frontend/src/components/MkWatermarkEditorDialog.vue')
-rw-r--r--packages/frontend/src/components/MkWatermarkEditorDialog.vue162
1 files changed, 108 insertions, 54 deletions
diff --git a/packages/frontend/src/components/MkWatermarkEditorDialog.vue b/packages/frontend/src/components/MkWatermarkEditorDialog.vue
index 3b3f20d8d1..6cd2111598 100644
--- a/packages/frontend/src/components/MkWatermarkEditorDialog.vue
+++ b/packages/frontend/src/components/MkWatermarkEditorDialog.vue
@@ -18,20 +18,21 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.root">
<div :class="$style.container">
- <div :class="$style.preview">
+ <div :class="[$style.preview, prefer.s.animation ? $style.animatedBg : null]">
<canvas ref="canvasEl" :class="$style.previewCanvas"></canvas>
<div :class="$style.previewContainer">
<div class="_acrylic" :class="$style.previewTitle">{{ i18n.ts.preview }}</div>
<div v-if="props.image == null" class="_acrylic" :class="$style.previewControls">
<button class="_button" :class="[$style.previewControlsButton, sampleImageType === '3_2' ? $style.active : null]" @click="sampleImageType = '3_2'"><i class="ti ti-crop-landscape"></i></button>
<button class="_button" :class="[$style.previewControlsButton, sampleImageType === '2_3' ? $style.active : null]" @click="sampleImageType = '2_3'"><i class="ti ti-crop-portrait"></i></button>
+ <button class="_button" :class="[$style.previewControlsButton]" @click="choiceImage"><i class="ti ti-upload"></i></button>
</div>
</div>
</div>
<div :class="$style.controls">
<div class="_spacer _gaps">
<div class="_gaps_s">
- <MkFolder v-for="(layer, i) in preset.layers" :key="layer.id" :defaultOpen="false" :canPage="false">
+ <MkFolder v-for="(layer, i) in layers" :key="layer.id" :defaultOpen="false" :canPage="false">
<template #label>
<div v-if="layer.type === 'text'">{{ i18n.ts._watermarkEditor.text }}</div>
<div v-if="layer.type === 'image'">{{ i18n.ts._watermarkEditor.image }}</div>
@@ -49,7 +50,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<XLayer
- v-model:layer="preset.layers[i]"
+ v-model:layer="layers[i]"
></XLayer>
</MkFolder>
@@ -64,8 +65,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<script setup lang="ts">
import { ref, useTemplateRef, watch, onMounted, onUnmounted, reactive, nextTick } from 'vue';
-import type { WatermarkPreset } from '@/utility/watermark.js';
-import { WatermarkRenderer } from '@/utility/watermark.js';
+import type { WatermarkLayers, WatermarkPreset } from '@/utility/watermark/WatermarkRenderer.js';
+import { WatermarkRenderer } from '@/utility/watermark/WatermarkRenderer.js';
import { i18n } from '@/i18n.js';
import MkModalWindow from '@/components/MkModalWindow.vue';
import MkSelect from '@/components/MkSelect.vue';
@@ -77,6 +78,7 @@ import { deepClone } from '@/utility/clone.js';
import { ensureSignin } from '@/i.js';
import { genId } from '@/utility/id.js';
import { useMkSelect } from '@/composables/use-mkselect.js';
+import { prefer } from '@/preferences.js';
const $i = ensureSignin();
@@ -161,18 +163,22 @@ function createCheckerLayer(): WatermarkPreset['layers'][number] {
}
const props = defineProps<{
+ presetEditMode?: boolean;
preset?: WatermarkPreset | null;
+ layers?: WatermarkLayers | null;
image?: File | null;
}>();
-const preset = reactive<WatermarkPreset>(deepClone(props.preset) ?? {
+const preset = deepClone(props.preset) ?? {
id: genId(),
name: '',
- layers: [],
-});
+};
+
+const layers = reactive<WatermarkLayers>(props.layers ?? []);
const emit = defineEmits<{
- (ev: 'ok', preset: WatermarkPreset): void;
+ (ev: 'ok', layers: WatermarkLayers): void;
+ (ev: 'presetOk', preset: WatermarkPreset): void;
(ev: 'cancel'): void;
(ev: 'closed'): void;
}>();
@@ -180,19 +186,21 @@ const emit = defineEmits<{
const dialog = useTemplateRef('dialog');
async function cancel() {
- const { canceled } = await os.confirm({
- type: 'question',
- text: i18n.ts._watermarkEditor.quitWithoutSaveConfirm,
- });
- if (canceled) return;
+ if (props.presetEditMode) {
+ const { canceled } = await os.confirm({
+ type: 'question',
+ text: i18n.ts._watermarkEditor.quitWithoutSaveConfirm,
+ });
+ if (canceled) return;
+ }
emit('cancel');
dialog.value?.close();
}
-watch(preset, async (newValue, oldValue) => {
+watch(layers, async (newValue, oldValue) => {
if (renderer != null) {
- renderer.setLayers(preset.layers);
+ renderer.render(layers);
}
}, { deep: true });
@@ -212,6 +220,7 @@ const sampleImage_2_3_loading = new Promise<void>(resolve => {
const sampleImageType = ref(props.image != null ? 'provided' : '3_2');
watch(sampleImageType, async () => {
+ if (sampleImageType.value === 'provided') return;
if (renderer != null) {
renderer.destroy(false);
renderer = null;
@@ -219,6 +228,20 @@ watch(sampleImageType, async () => {
}
});
+let imageFile = props.image;
+
+async function choiceImage() {
+ const files = await os.chooseFileFromPc({ multiple: false });
+ if (files.length === 0) return;
+ imageFile = files[0];
+ sampleImageType.value = 'provided';
+ if (renderer != null) {
+ renderer.destroy(false);
+ renderer = null;
+ initRenderer();
+ }
+}
+
let renderer: WatermarkRenderer | null = null;
let imageBitmap: ImageBitmap | null = null;
@@ -239,8 +262,8 @@ async function initRenderer() {
renderHeight: 1500,
image: sampleImage_2_3,
});
- } else if (props.image != null) {
- imageBitmap = await window.createImageBitmap(props.image);
+ } else if (imageFile != null) {
+ imageBitmap = await window.createImageBitmap(imageFile);
const MAX_W = 1000;
const MAX_H = 1000;
@@ -249,8 +272,8 @@ async function initRenderer() {
if (w > MAX_W || h > MAX_H) {
const scale = Math.min(MAX_W / w, MAX_H / h);
- w *= scale;
- h *= scale;
+ w = Math.floor(w * scale);
+ h = Math.floor(h * scale);
}
renderer = new WatermarkRenderer({
@@ -261,9 +284,7 @@ async function initRenderer() {
});
}
- await renderer!.setLayers(preset.layers);
-
- renderer!.render();
+ await renderer!.render(layers);
}
onMounted(async () => {
@@ -274,7 +295,15 @@ onMounted(async () => {
await sampleImage_3_2_loading;
await sampleImage_2_3_loading;
- await initRenderer();
+ try {
+ await initRenderer();
+ } catch (err) {
+ console.error(err);
+ os.alert({
+ type: 'error',
+ text: i18n.ts._watermarkEditor.failedToLoadImage,
+ });
+ }
closeWaiting();
});
@@ -291,77 +320,93 @@ onUnmounted(() => {
});
async function save() {
- const { canceled, result: name } = await os.inputText({
- title: i18n.ts.name,
- default: preset.name,
- });
- if (canceled) return;
+ if (props.presetEditMode) {
+ const { canceled, result: name } = await os.inputText({
+ title: i18n.ts.name,
+ default: preset.name,
+ });
+ if (canceled) return;
- preset.name = name || '';
+ preset.name = name || '';
- dialog.value?.close();
- if (renderer != null) {
- renderer.destroy();
- renderer = null;
- }
+ dialog.value?.close();
+ if (renderer != null) {
+ renderer.destroy();
+ renderer = null;
+ }
- emit('ok', preset);
+ emit('presetOk', {
+ ...preset,
+ layers: deepClone(layers),
+ });
+ } else {
+ dialog.value?.close();
+ if (renderer != null) {
+ renderer.destroy();
+ renderer = null;
+ }
+
+ emit('ok', layers);
+ }
}
function addLayer(ev: MouseEvent) {
os.popupMenu([{
text: i18n.ts._watermarkEditor.text,
action: () => {
- preset.layers.push(createTextLayer());
+ layers.push(createTextLayer());
},
}, {
text: i18n.ts._watermarkEditor.image,
action: () => {
- preset.layers.push(createImageLayer());
+ layers.push(createImageLayer());
},
}, {
text: i18n.ts._watermarkEditor.qr,
action: () => {
- preset.layers.push(createQrLayer());
+ layers.push(createQrLayer());
},
}, {
text: i18n.ts._watermarkEditor.stripe,
action: () => {
- preset.layers.push(createStripeLayer());
+ layers.push(createStripeLayer());
},
}, {
text: i18n.ts._watermarkEditor.polkadot,
action: () => {
- preset.layers.push(createPolkadotLayer());
+ layers.push(createPolkadotLayer());
},
}, {
text: i18n.ts._watermarkEditor.checker,
action: () => {
- preset.layers.push(createCheckerLayer());
+ layers.push(createCheckerLayer());
},
}], ev.currentTarget ?? ev.target);
}
function swapUpLayer(layer: WatermarkPreset['layers'][number]) {
- const index = preset.layers.findIndex(l => l.id === layer.id);
+ const index = layers.findIndex(l => l.id === layer.id);
if (index > 0) {
- const tmp = preset.layers[index - 1];
- preset.layers[index - 1] = preset.layers[index];
- preset.layers[index] = tmp;
+ const tmp = layers[index - 1];
+ layers[index - 1] = layers[index];
+ layers[index] = tmp;
}
}
function swapDownLayer(layer: WatermarkPreset['layers'][number]) {
- const index = preset.layers.findIndex(l => l.id === layer.id);
- if (index < preset.layers.length - 1) {
- const tmp = preset.layers[index + 1];
- preset.layers[index + 1] = preset.layers[index];
- preset.layers[index] = tmp;
+ const index = layers.findIndex(l => l.id === layer.id);
+ if (index < layers.length - 1) {
+ const tmp = layers[index + 1];
+ layers[index + 1] = layers[index];
+ layers[index] = tmp;
}
}
function removeLayer(layer: WatermarkPreset['layers'][number]) {
- preset.layers = preset.layers.filter(l => l.id !== layer.id);
+ const index = layers.findIndex(l => l.id === layer.id);
+ if (index !== -1) {
+ layers.splice(index, 1);
+ }
}
</script>
@@ -380,8 +425,17 @@ function removeLayer(layer: WatermarkPreset['layers'][number]) {
.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 {