This commit is contained in:
syuilo 2025-10-30 20:16:28 +09:00
parent 0050b873c6
commit 400c7cc408
6 changed files with 102 additions and 69 deletions

4
locales/index.d.ts vendored
View File

@ -5618,6 +5618,10 @@ export interface Locale extends ILocale {
* *
*/ */
"labelThickness": string; "labelThickness": string;
/**
*
*/
"labelScale": string;
/** /**
* *
*/ */

View File

@ -1401,6 +1401,7 @@ _imageLabelEditor:
title: "ラベルの編集" title: "ラベルの編集"
frameThickness: "フレームの幅" frameThickness: "フレームの幅"
labelThickness: "ラベルの幅" labelThickness: "ラベルの幅"
labelScale: "ラベルのスケール"
centered: "中央揃え" centered: "中央揃え"
captionMain: "キャプション(大)" captionMain: "キャプション(大)"
captionSub: "キャプション(小)" captionSub: "キャプション(小)"

View File

@ -34,10 +34,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts._imageLabelEditor.frameThickness }}</template> <template #label>{{ i18n.ts._imageLabelEditor.frameThickness }}</template>
</MkRange> </MkRange>
<MkRange v-model="frame.labelThickness" :min="0.1" :max="0.3" :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._imageLabelEditor.labelThickness }}</template>
</MkRange> </MkRange>
<MkRange v-model="frame.labelScale" :min="0.5" :max="2.0" :step="0.01" :continuousUpdate="true">
<template #label>{{ i18n.ts._imageLabelEditor.labelScale }}</template>
</MkRange>
<MkSwitch v-model="frame.centered"> <MkSwitch v-model="frame.centered">
<template #label>{{ i18n.ts._imageLabelEditor.centered }}</template> <template #label>{{ i18n.ts._imageLabelEditor.centered }}</template>
</MkSwitch> </MkSwitch>
@ -74,6 +78,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script setup lang="ts"> <script setup lang="ts">
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 type { ImageLabelParams } from '@/utility/image-label-renderer.js'; import type { ImageLabelParams } from '@/utility/image-label-renderer.js';
import { ImageLabelRenderer } from '@/utility/image-label-renderer.js'; import { ImageLabelRenderer } from '@/utility/image-label-renderer.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
@ -114,6 +119,7 @@ const frame = reactive<ImageLabelParams>(deepClone(props.frame) ?? {
style: 'frame', style: 'frame',
frameThickness: 0.05, frameThickness: 0.05,
labelThickness: 0.2, labelThickness: 0.2,
labelScale: 1.0,
title: 'Untitled by @syuilo', title: 'Untitled by @syuilo',
text: '{mm}mm f/{f} {s}s ISO{iso}', text: '{mm}mm f/{f} {s}s ISO{iso}',
centered: false, centered: false,
@ -132,10 +138,14 @@ async function cancel() {
dialog.value?.close(); dialog.value?.close();
} }
watch(frame, async (newValue, oldValue) => { const updateThrottled = throttle(100, () => {
if (renderer != null) { if (renderer != null) {
renderer.update(frame); renderer.update(frame);
} }
});
watch(frame, async (newValue, oldValue) => {
updateThrottled();
}, { deep: true }); }, { deep: true });
const canvasEl = useTemplateRef('canvasEl'); const canvasEl = useTemplateRef('canvasEl');

View File

@ -11,10 +11,10 @@ uniform sampler2D in_texture;
uniform vec2 in_resolution; uniform vec2 in_resolution;
uniform sampler2D u_image; uniform sampler2D u_image;
uniform sampler2D u_label; uniform sampler2D u_label;
uniform vec2 u_labelResolution; uniform float u_paddingTop;
uniform bool u_labelEnabled; uniform float u_paddingBottom;
uniform float u_imageMarginX; uniform float u_paddingLeft;
uniform float u_imageMarginY; uniform float u_paddingRight;
out vec4 out_color; out vec4 out_color;
float remap(float value, float inputMin, float inputMax, float outputMin, float outputMax) { float remap(float value, float inputMin, float inputMax, float outputMin, float outputMax) {
@ -22,19 +22,20 @@ float remap(float value, float inputMin, float inputMax, float outputMin, float
} }
void main() { void main() {
float labelRatio = u_labelEnabled ? (u_labelResolution.y / in_resolution.y) : 0.0;
vec4 image_color = texture(u_image, vec2( vec4 image_color = texture(u_image, vec2(
remap(in_uv.x, u_imageMarginX, 1.0 - u_imageMarginX, 0.0, 1.0), remap(in_uv.x, u_paddingLeft, 1.0 - u_paddingRight, 0.0, 1.0),
remap(in_uv.y, u_imageMarginY, 1.0 - labelRatio, 0.0, 1.0) remap(in_uv.y, u_paddingTop, 1.0 - u_paddingBottom, 0.0, 1.0)
)); ));
vec4 label_color = texture(u_label, (in_uv - vec2(0.0, 1.0 - labelRatio)) / vec2(1.0, labelRatio)); vec4 label_color = texture(u_label, vec2(
in_uv.x,
remap(in_uv.y, 1.0 - u_paddingBottom, 1.0, 0.0, 1.0)
));
if (in_uv.y > (1.0 - labelRatio)) { if (in_uv.y > (1.0 - u_paddingBottom)) {
out_color = label_color; out_color = label_color;
} else { } else {
if (in_uv.y > u_imageMarginY && in_uv.x > u_imageMarginX && in_uv.x < (1.0 - u_imageMarginX)) { if (in_uv.y > u_paddingTop && in_uv.x > u_paddingLeft && in_uv.x < (1.0 - u_paddingRight)) {
out_color = image_color; out_color = image_color;
} else { } else {
out_color = vec4(1.0, 1.0, 1.0, 1.0); out_color = vec4(1.0, 1.0, 1.0, 1.0);

View File

@ -10,7 +10,7 @@ export const FX_label = defineImageEffectorFx({
id: 'label', id: 'label',
name: '(internal)', name: '(internal)',
shader, shader,
uniforms: ['image', 'label', 'labelResolution', 'labelEnabled', 'imageMarginX', 'imageMarginY'] as const, uniforms: ['image', 'label', 'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight'] as const,
params: { params: {
image: { image: {
type: 'textureRef', type: 'textureRef',
@ -20,15 +20,27 @@ export const FX_label = defineImageEffectorFx({
type: 'textureRef', type: 'textureRef',
default: null, default: null,
}, },
imageMarginX: { paddingTop: {
type: 'number', type: 'number',
default: 0.05, default: 0,
max: 1, max: 1,
min: 0, min: 0,
}, },
imageMarginY: { paddingBottom: {
type: 'number', type: 'number',
default: 0.05, default: 0,
max: 1,
min: 0,
},
paddingLeft: {
type: 'number',
default: 0,
max: 1,
min: 0,
},
paddingRight: {
type: 'number',
default: 0,
max: 1, max: 1,
min: 0, min: 0,
}, },
@ -39,19 +51,16 @@ export const FX_label = defineImageEffectorFx({
gl.bindTexture(gl.TEXTURE_2D, image.texture); gl.bindTexture(gl.TEXTURE_2D, image.texture);
gl.uniform1i(u.image, 1); gl.uniform1i(u.image, 1);
gl.uniform1f(u.imageMarginX, params.imageMarginX); gl.uniform1f(u.paddingTop, params.paddingTop);
gl.uniform1f(u.imageMarginY, params.imageMarginY); gl.uniform1f(u.paddingBottom, params.paddingBottom);
gl.uniform1f(u.paddingLeft, params.paddingLeft);
gl.uniform1f(u.paddingRight, params.paddingRight);
const label = textures.label; const label = textures.label;
if (label) { if (label) {
gl.activeTexture(gl.TEXTURE2); gl.activeTexture(gl.TEXTURE2);
gl.bindTexture(gl.TEXTURE_2D, label.texture); gl.bindTexture(gl.TEXTURE_2D, label.texture);
gl.uniform1i(u.label, 2); gl.uniform1i(u.label, 2);
gl.uniform2f(u.labelResolution, label.width, label.height);
gl.uniform1i(u.labelEnabled, 1);
} else {
gl.uniform1i(u.labelEnabled, 0);
} }
}, },
}); });

View File

@ -19,6 +19,7 @@ export type ImageLabelParams = {
style: 'frame' | 'frameLess'; style: 'frame' | 'frameLess';
frameThickness: number; frameThickness: number;
labelThickness: number; labelThickness: number;
labelScale: number;
title: string; title: string;
text: string; text: string;
centered: boolean; centered: boolean;
@ -96,11 +97,12 @@ export class ImageLabelRenderer {
const labelCanvasCtx = window.document.createElement('canvas').getContext('2d')!; const labelCanvasCtx = window.document.createElement('canvas').getContext('2d')!;
labelCanvasCtx.canvas.width = renderWidth; labelCanvasCtx.canvas.width = renderWidth;
labelCanvasCtx.canvas.height = paddingBottom; labelCanvasCtx.canvas.height = paddingBottom;
const fontSize = labelCanvasCtx.canvas.height / 6; const scaleBase = imageAreaH * params.labelScale;
const fontSize = scaleBase / 30;
const textsMarginLeft = Math.max(fontSize * 2, paddingLeft); const textsMarginLeft = Math.max(fontSize * 2, paddingLeft);
const textsMarginRight = textsMarginLeft; const textsMarginRight = textsMarginLeft;
const withQrCode = params.withQrCode; const withQrCode = params.withQrCode;
const qrSize = labelCanvasCtx.canvas.height * 0.6; const qrSize = scaleBase * 0.1;
const qrMarginRight = Math.max((labelCanvasCtx.canvas.height - qrSize) / 2, paddingRight); const qrMarginRight = Math.max((labelCanvasCtx.canvas.height - qrSize) / 2, paddingRight);
labelCanvasCtx.fillStyle = '#ffffff'; labelCanvasCtx.fillStyle = '#ffffff';
@ -135,49 +137,53 @@ export class ImageLabelRenderer {
const $i = ensureSignin(); const $i = ensureSignin();
if (withQrCode) { if (withQrCode) {
const qrCodeInstance = new QRCodeStyling({ try {
width: labelCanvasCtx.canvas.height, const qrCodeInstance = new QRCodeStyling({
height: labelCanvasCtx.canvas.height, width: labelCanvasCtx.canvas.height,
margin: 0, height: labelCanvasCtx.canvas.height,
type: 'canvas', margin: 0,
data: `${url}/users/${$i.id}`, type: 'canvas',
//image: $i.avatarUrl, data: `${url}/users/${$i.id}`,
qrOptions: { //image: $i.avatarUrl,
typeNumber: 0, qrOptions: {
mode: 'Byte', typeNumber: 0,
errorCorrectionLevel: 'H', mode: 'Byte',
}, errorCorrectionLevel: 'H',
imageOptions: { },
hideBackgroundDots: true, imageOptions: {
imageSize: 0.3, hideBackgroundDots: true,
margin: 16, imageSize: 0.3,
crossOrigin: 'anonymous', margin: 16,
}, crossOrigin: 'anonymous',
dotsOptions: { },
type: 'dots', dotsOptions: {
roundSize: false, type: 'dots',
}, roundSize: false,
cornersDotOptions: { },
type: 'dot', cornersDotOptions: {
}, type: 'dot',
cornersSquareOptions: { },
type: 'extra-rounded', cornersSquareOptions: {
}, type: 'extra-rounded',
}); },
});
const blob = await qrCodeInstance.getRawData('png') as Blob | null; const blob = await qrCodeInstance.getRawData('png') as Blob | null;
if (blob == null) throw new Error('Failed to generate QR code'); if (blob == null) throw new Error('Failed to generate QR code');
const qrImageBitmap = await window.createImageBitmap(blob); const qrImageBitmap = await window.createImageBitmap(blob);
labelCanvasCtx.drawImage( labelCanvasCtx.drawImage(
qrImageBitmap, qrImageBitmap,
labelCanvasCtx.canvas.width - qrSize - qrMarginRight, labelCanvasCtx.canvas.width - qrSize - qrMarginRight,
(labelCanvasCtx.canvas.height - qrSize) / 2, (labelCanvasCtx.canvas.height - qrSize) / 2,
qrSize, qrSize,
qrSize, qrSize,
); );
qrImageBitmap.close(); qrImageBitmap.close();
} catch (err) {
// nop
}
} }
const data = labelCanvasCtx.getImageData(0, 0, labelCanvasCtx.canvas.width, labelCanvasCtx.canvas.height); const data = labelCanvasCtx.getImageData(0, 0, labelCanvasCtx.canvas.width, labelCanvasCtx.canvas.height);
@ -192,8 +198,10 @@ export class ImageLabelRenderer {
params: { params: {
image: 'image', image: 'image',
label: 'label', label: 'label',
imageMarginX: paddingLeft / renderWidth, paddingLeft: paddingLeft / renderWidth,
imageMarginY: paddingTop / renderHeight, paddingRight: paddingRight / renderWidth,
paddingTop: paddingTop / renderHeight,
paddingBottom: paddingBottom / renderHeight,
}, },
}]); }]);
} }