This commit is contained in:
syuilo 2025-10-31 09:17:59 +09:00
parent d550e42b0d
commit 8847a4aa6e
8 changed files with 44 additions and 45 deletions

4
locales/index.d.ts vendored
View File

@ -5605,7 +5605,7 @@ export interface Locale extends ILocale {
* *
*/ */
"deviceInfoDescription": string; "deviceInfoDescription": string;
"_imageLabelEditor": { "_imageFrameEditor": {
/** /**
* *
*/ */
@ -5613,7 +5613,7 @@ export interface Locale extends ILocale {
/** /**
* *
*/ */
"frameThickness": string; "borderThickness": string;
/** /**
* *
*/ */

View File

@ -1396,10 +1396,11 @@ scheduled: "予約"
widgets: "ウィジェット" widgets: "ウィジェット"
deviceInfo: "デバイス情報" deviceInfo: "デバイス情報"
deviceInfoDescription: "技術的なお問い合わせの際に、以下の情報を併記すると問題の解決に役立つことがあります。" deviceInfoDescription: "技術的なお問い合わせの際に、以下の情報を併記すると問題の解決に役立つことがあります。"
frame: "フレーム"
_imageLabelEditor: _imageFrameEditor:
title: "ラベルの編集" title: "フレームの編集"
frameThickness: "フレームの幅" borderThickness: "フチの幅"
labelThickness: "ラベルの幅" labelThickness: "ラベルの幅"
labelScale: "ラベルのスケール" labelScale: "ラベルのスケール"
centered: "中央揃え" centered: "中央揃え"

View File

@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@ok="save()" @ok="save()"
@closed="emit('closed')" @closed="emit('closed')"
> >
<template #header><i class="ti ti-photo"></i> {{ i18n.ts._imageLabelEditor.title }}</template> <template #header><i class="ti ti-photo"></i> {{ i18n.ts._imageFrameEditor.title }}</template>
<div :class="$style.root"> <div :class="$style.root">
<div :class="$style.container"> <div :class="$style.container">
@ -31,36 +31,36 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
<div :class="$style.controls"> <div :class="$style.controls">
<div class="_spacer _gaps"> <div class="_spacer _gaps">
<MkRange v-model="frame.frameThickness" :min="0" :max="0.2" :step="0.01" :continuousUpdate="true"> <MkRange v-model="frame.borderThickness" :min="0" :max="0.2" :step="0.01" :continuousUpdate="true">
<template #label>{{ i18n.ts._imageLabelEditor.frameThickness }}</template> <template #label>{{ i18n.ts._imageFrameEditor.borderThickness }}</template>
</MkRange> </MkRange>
<MkRange v-model="frame.labelThickness" :min="0.01" :max="0.5" :step="0.01" :continuousUpdate="true"> <MkRange v-model="frame.labelThickness" :min="0.01" :max="0.5" :step="0.01" :continuousUpdate="true">
<template #label>{{ i18n.ts._imageLabelEditor.labelThickness }}</template> <template #label>{{ i18n.ts._imageFrameEditor.labelThickness }}</template>
</MkRange> </MkRange>
<MkRange v-model="frame.labelScale" :min="0.5" :max="2.0" :step="0.01" :continuousUpdate="true"> <MkRange v-model="frame.labelScale" :min="0.5" :max="2.0" :step="0.01" :continuousUpdate="true">
<template #label>{{ i18n.ts._imageLabelEditor.labelScale }}</template> <template #label>{{ i18n.ts._imageFrameEditor.labelScale }}</template>
</MkRange> </MkRange>
<MkSwitch v-model="frame.centered"> <MkSwitch v-model="frame.centered">
<template #label>{{ i18n.ts._imageLabelEditor.centered }}</template> <template #label>{{ i18n.ts._imageFrameEditor.centered }}</template>
</MkSwitch> </MkSwitch>
<MkInput v-model="frame.title"> <MkInput v-model="frame.title">
<template #label>{{ i18n.ts._imageLabelEditor.captionMain }}</template> <template #label>{{ i18n.ts._imageFrameEditor.captionMain }}</template>
</MkInput> </MkInput>
<MkTextarea v-model="frame.text"> <MkTextarea v-model="frame.text">
<template #label>{{ i18n.ts._imageLabelEditor.captionSub }}</template> <template #label>{{ i18n.ts._imageFrameEditor.captionSub }}</template>
</MkTextarea> </MkTextarea>
<MkSwitch v-model="frame.withQrCode"> <MkSwitch v-model="frame.withQrCode">
<template #label>{{ i18n.ts._imageLabelEditor.withQrCode }}</template> <template #label>{{ i18n.ts._imageFrameEditor.withQrCode }}</template>
</MkSwitch> </MkSwitch>
<MkInfo> <MkInfo>
<div>{{ i18n.ts._imageLabelEditor.availableVariables }}:</div> <div>{{ i18n.ts._imageFrameEditor.availableVariables }}:</div>
<div><code class="_selectableAtomic">{date}</code> - 撮影日時</div> <div><code class="_selectableAtomic">{date}</code> - 撮影日時</div>
<div><code class="_selectableAtomic">{model}</code> - カメラモデル</div> <div><code class="_selectableAtomic">{model}</code> - カメラモデル</div>
<div><code class="_selectableAtomic">{lensModel}</code> - レンズモデル</div> <div><code class="_selectableAtomic">{lensModel}</code> - レンズモデル</div>
@ -80,8 +80,8 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref, useTemplateRef, watch, onMounted, onUnmounted, reactive, nextTick } from 'vue'; import { ref, useTemplateRef, watch, onMounted, onUnmounted, reactive, nextTick } from 'vue';
import ExifReader from 'exifreader'; import ExifReader from 'exifreader';
import { throttle } from 'throttle-debounce'; import { throttle } from 'throttle-debounce';
import type { ImageLabelParams } from '@/utility/image-label-renderer.js'; import type { ImageFrameParams } from '@/utility/image-frame-renderer.js';
import { ImageLabelRenderer } from '@/utility/image-label-renderer.js'; import { ImageFrameRenderer } from '@/utility/image-frame-renderer.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import MkModalWindow from '@/components/MkModalWindow.vue'; import MkModalWindow from '@/components/MkModalWindow.vue';
import MkSelect from '@/components/MkSelect.vue'; import MkSelect from '@/components/MkSelect.vue';
@ -112,13 +112,12 @@ const EXIF_MOCK = {
} satisfies ExifReader.Tags; } satisfies ExifReader.Tags;
const props = defineProps<{ const props = defineProps<{
frame?: ImageLabelParams | null; frame?: ImageFrameParams | null;
image?: File | null; image?: File | null;
}>(); }>();
const frame = reactive<ImageLabelParams>(deepClone(props.frame) ?? { const frame = reactive<ImageFrameParams>(deepClone(props.frame) ?? {
style: 'frame', borderThickness: 0.05,
frameThickness: 0.05,
labelThickness: 0.2, labelThickness: 0.2,
labelScale: 1.0, labelScale: 1.0,
title: 'Untitled by @syuilo', title: 'Untitled by @syuilo',
@ -128,7 +127,7 @@ const frame = reactive<ImageLabelParams>(deepClone(props.frame) ?? {
}); });
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'ok', frame: ImageLabelParams): void; (ev: 'ok', frame: ImageFrameParams): void;
(ev: 'cancel'): void; (ev: 'cancel'): void;
(ev: 'closed'): void; (ev: 'closed'): void;
}>(); }>();
@ -187,21 +186,21 @@ async function choiceImage() {
} }
} }
let renderer: ImageLabelRenderer | null = null; let renderer: ImageFrameRenderer | null = null;
let imageBitmap: ImageBitmap | null = null; let imageBitmap: ImageBitmap | null = null;
async function initRenderer() { async function initRenderer() {
if (canvasEl.value == null) return; if (canvasEl.value == null) return;
if (sampleImageType.value === '3_2') { if (sampleImageType.value === '3_2') {
renderer = new ImageLabelRenderer({ renderer = new ImageFrameRenderer({
canvas: canvasEl.value, canvas: canvasEl.value,
image: sampleImage_3_2, image: sampleImage_3_2,
exif: EXIF_MOCK, exif: EXIF_MOCK,
renderAsPreview: true, renderAsPreview: true,
}); });
} else if (sampleImageType.value === '2_3') { } else if (sampleImageType.value === '2_3') {
renderer = new ImageLabelRenderer({ renderer = new ImageFrameRenderer({
canvas: canvasEl.value, canvas: canvasEl.value,
image: sampleImage_2_3, image: sampleImage_2_3,
exif: EXIF_MOCK, exif: EXIF_MOCK,
@ -212,7 +211,7 @@ async function initRenderer() {
const exif = ExifReader.load(await imageFile.arrayBuffer()); const exif = ExifReader.load(await imageFile.arrayBuffer());
renderer = new ImageLabelRenderer({ renderer = new ImageFrameRenderer({
canvas: canvasEl.value, canvas: canvasEl.value,
image: imageBitmap, image: imageBitmap,
exif: exif, exif: exif,

View File

@ -18,7 +18,7 @@ import { uploadFile, UploadAbortedError } from '@/utility/drive.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { ensureSignin } from '@/i.js'; import { ensureSignin } from '@/i.js';
import { WatermarkRenderer } from '@/utility/watermark.js'; import { WatermarkRenderer } from '@/utility/watermark.js';
import { ImageLabelRenderer } from '@/utility/image-label-renderer.js'; import { ImageFrameRenderer } from '@/utility/image-frame-renderer.js';
export type UploaderFeatures = { export type UploaderFeatures = {
imageEditing?: boolean; imageEditing?: boolean;
@ -575,7 +575,7 @@ export function useUploader(options: {
const exif = await ExifReader.load(await item.file.arrayBuffer()); const exif = await ExifReader.load(await item.file.arrayBuffer());
const labelRenderer = new ImageLabelRenderer({ const labelRenderer = new ImageFrameRenderer({
canvas: canvas, canvas: canvas,
image: await window.createImageBitmap(preprocessedFile), image: await window.createImageBitmap(preprocessedFile),
exif, exif,

View File

@ -127,7 +127,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['label', 'frame', 'credit', 'metadata']"> <SearchMarker :keywords="['label', 'frame', 'credit', 'metadata']">
<MkFolder> <MkFolder>
<template #icon><i class="ti ti-photo"></i></template> <template #icon><i class="ti ti-photo"></i></template>
<template #label><SearchLabel>{{ i18n.ts.label }}</SearchLabel></template> <template #label><SearchLabel>{{ i18n.ts.frame }}</SearchLabel></template>
<div class="_gaps"> <div class="_gaps">
<div class="_gaps_s"> <div class="_gaps_s">
@ -141,7 +141,7 @@ SPDX-License-Identifier: AGPL-3.0-only
/> />
--> -->
<MkButton iconOnly rounded style="margin: 0 auto;" @click="addImageLabelPreset"><i class="ti ti-plus"></i></MkButton> <MkButton iconOnly rounded style="margin: 0 auto;" @click="addImageFramePreset"><i class="ti ti-plus"></i></MkButton>
<!-- <!--
<SearchMarker :keywords="['sync', 'watermark', 'preset', 'devices']"> <SearchMarker :keywords="['sync', 'watermark', 'preset', 'devices']">
@ -342,8 +342,8 @@ function onDeleteWatermarkPreset(id: string) {
} }
} }
async function addImageLabelPreset() { async function addImageFramePreset() {
const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkImageLabelEditorDialog.vue').then(x => x.default), { const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkImageFrameEditorDialog.vue').then(x => x.default), {
}, { }, {
ok: (preset: any) => { ok: (preset: any) => {
//prefer.commit('imageLabelPresets', [...prefer.s.imageLabelPresets, preset]); //prefer.commit('imageLabelPresets', [...prefer.s.imageLabelPresets, preset]);

View File

@ -6,8 +6,8 @@
import { defineImageEffectorFx } from '../ImageEffector.js'; import { defineImageEffectorFx } from '../ImageEffector.js';
import shader from './label.glsl'; import shader from './label.glsl';
export const FX_label = defineImageEffectorFx({ export const FX_frame = defineImageEffectorFx({
id: 'label', id: 'frame',
name: '(internal)', name: '(internal)',
shader, shader,
uniforms: ['image', 'label', 'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight'] as const, uniforms: ['image', 'label', 'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight'] as const,

View File

@ -6,18 +6,17 @@
import QRCodeStyling from 'qr-code-styling'; import QRCodeStyling from 'qr-code-styling';
import { url } from '@@/js/config.js'; import { url } from '@@/js/config.js';
import ExifReader from 'exifreader'; import ExifReader from 'exifreader';
import { FX_label } from './image-effector/fxs/label.js'; import { FX_frame } from './image-effector/fxs/frame.js';
import type { ImageEffectorFx, ImageEffectorLayer } from '@/utility/image-effector/ImageEffector.js'; import type { ImageEffectorFx, ImageEffectorLayer } from '@/utility/image-effector/ImageEffector.js';
import { ImageEffector } from '@/utility/image-effector/ImageEffector.js'; import { ImageEffector } from '@/utility/image-effector/ImageEffector.js';
import { ensureSignin } from '@/i.js'; import { ensureSignin } from '@/i.js';
const FXS = [ const FXS = [
FX_label, FX_frame,
] as const satisfies ImageEffectorFx<string, any>[]; ] as const satisfies ImageEffectorFx<string, any>[];
export type ImageLabelParams = { export type ImageFrameParams = {
style: 'frame' | 'frameLess'; borderThickness: number;
frameThickness: number;
labelThickness: number; labelThickness: number;
labelScale: number; labelScale: number;
title: string; title: string;
@ -29,7 +28,7 @@ export type ImageLabelParams = {
borderRadius: number; // TODO borderRadius: number; // TODO
}; };
export class ImageLabelRenderer { export class ImageFrameRenderer {
private effector: ImageEffector<typeof FXS>; private effector: ImageEffector<typeof FXS>;
private image: HTMLImageElement | ImageBitmap; private image: HTMLImageElement | ImageBitmap;
private exif: ExifReader.Tags; private exif: ExifReader.Tags;
@ -74,7 +73,7 @@ export class ImageLabelRenderer {
}); });
} }
public async updateAndRender(params: ImageLabelParams): Promise<void> { public async updateAndRender(params: ImageFrameParams): Promise<void> {
let imageAreaW = this.image.width; let imageAreaW = this.image.width;
let imageAreaH = this.image.height; let imageAreaH = this.image.height;
@ -89,9 +88,9 @@ export class ImageLabelRenderer {
} }
} }
const paddingTop = Math.floor(imageAreaH * params.frameThickness); const paddingTop = Math.floor(imageAreaH * params.borderThickness);
const paddingLeft = Math.floor(imageAreaH * params.frameThickness); const paddingLeft = Math.floor(imageAreaH * params.borderThickness);
const paddingRight = Math.floor(imageAreaH * params.frameThickness); const paddingRight = Math.floor(imageAreaH * params.borderThickness);
const paddingBottom = Math.floor(imageAreaH * params.labelThickness); const paddingBottom = Math.floor(imageAreaH * params.labelThickness);
const renderWidth = imageAreaW + paddingLeft + paddingRight; const renderWidth = imageAreaW + paddingLeft + paddingRight;
const renderHeight = imageAreaH + paddingTop + paddingBottom; const renderHeight = imageAreaH + paddingTop + paddingBottom;
@ -196,7 +195,7 @@ export class ImageLabelRenderer {
this.effector.changeResolution(renderWidth, renderHeight); this.effector.changeResolution(renderWidth, renderHeight);
await this.effector.setLayersAndRender([{ await this.effector.setLayersAndRender([{
fxId: 'label', fxId: 'frame',
id: 'a', id: 'a',
params: { params: {
image: 'image', image: 'image',