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;
/**
*
*/
"labelScale": string;
/**
*
*/

View File

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

View File

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

View File

@ -11,10 +11,10 @@ uniform sampler2D in_texture;
uniform vec2 in_resolution;
uniform sampler2D u_image;
uniform sampler2D u_label;
uniform vec2 u_labelResolution;
uniform bool u_labelEnabled;
uniform float u_imageMarginX;
uniform float u_imageMarginY;
uniform float u_paddingTop;
uniform float u_paddingBottom;
uniform float u_paddingLeft;
uniform float u_paddingRight;
out vec4 out_color;
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() {
float labelRatio = u_labelEnabled ? (u_labelResolution.y / in_resolution.y) : 0.0;
vec4 image_color = texture(u_image, vec2(
remap(in_uv.x, u_imageMarginX, 1.0 - u_imageMarginX, 0.0, 1.0),
remap(in_uv.y, u_imageMarginY, 1.0 - labelRatio, 0.0, 1.0)
remap(in_uv.x, u_paddingLeft, 1.0 - u_paddingRight, 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;
} 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;
} else {
out_color = vec4(1.0, 1.0, 1.0, 1.0);

View File

@ -10,7 +10,7 @@ export const FX_label = defineImageEffectorFx({
id: 'label',
name: '(internal)',
shader,
uniforms: ['image', 'label', 'labelResolution', 'labelEnabled', 'imageMarginX', 'imageMarginY'] as const,
uniforms: ['image', 'label', 'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight'] as const,
params: {
image: {
type: 'textureRef',
@ -20,15 +20,27 @@ export const FX_label = defineImageEffectorFx({
type: 'textureRef',
default: null,
},
imageMarginX: {
paddingTop: {
type: 'number',
default: 0.05,
default: 0,
max: 1,
min: 0,
},
imageMarginY: {
paddingBottom: {
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,
min: 0,
},
@ -39,19 +51,16 @@ export const FX_label = defineImageEffectorFx({
gl.bindTexture(gl.TEXTURE_2D, image.texture);
gl.uniform1i(u.image, 1);
gl.uniform1f(u.imageMarginX, params.imageMarginX);
gl.uniform1f(u.imageMarginY, params.imageMarginY);
gl.uniform1f(u.paddingTop, params.paddingTop);
gl.uniform1f(u.paddingBottom, params.paddingBottom);
gl.uniform1f(u.paddingLeft, params.paddingLeft);
gl.uniform1f(u.paddingRight, params.paddingRight);
const label = textures.label;
if (label) {
gl.activeTexture(gl.TEXTURE2);
gl.bindTexture(gl.TEXTURE_2D, label.texture);
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';
frameThickness: number;
labelThickness: number;
labelScale: number;
title: string;
text: string;
centered: boolean;
@ -96,11 +97,12 @@ export class ImageLabelRenderer {
const labelCanvasCtx = window.document.createElement('canvas').getContext('2d')!;
labelCanvasCtx.canvas.width = renderWidth;
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 textsMarginRight = textsMarginLeft;
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);
labelCanvasCtx.fillStyle = '#ffffff';
@ -135,49 +137,53 @@ export class ImageLabelRenderer {
const $i = ensureSignin();
if (withQrCode) {
const qrCodeInstance = new QRCodeStyling({
width: labelCanvasCtx.canvas.height,
height: labelCanvasCtx.canvas.height,
margin: 0,
type: 'canvas',
data: `${url}/users/${$i.id}`,
//image: $i.avatarUrl,
qrOptions: {
typeNumber: 0,
mode: 'Byte',
errorCorrectionLevel: 'H',
},
imageOptions: {
hideBackgroundDots: true,
imageSize: 0.3,
margin: 16,
crossOrigin: 'anonymous',
},
dotsOptions: {
type: 'dots',
roundSize: false,
},
cornersDotOptions: {
type: 'dot',
},
cornersSquareOptions: {
type: 'extra-rounded',
},
});
try {
const qrCodeInstance = new QRCodeStyling({
width: labelCanvasCtx.canvas.height,
height: labelCanvasCtx.canvas.height,
margin: 0,
type: 'canvas',
data: `${url}/users/${$i.id}`,
//image: $i.avatarUrl,
qrOptions: {
typeNumber: 0,
mode: 'Byte',
errorCorrectionLevel: 'H',
},
imageOptions: {
hideBackgroundDots: true,
imageSize: 0.3,
margin: 16,
crossOrigin: 'anonymous',
},
dotsOptions: {
type: 'dots',
roundSize: false,
},
cornersDotOptions: {
type: 'dot',
},
cornersSquareOptions: {
type: 'extra-rounded',
},
});
const blob = await qrCodeInstance.getRawData('png') as Blob | null;
if (blob == null) throw new Error('Failed to generate QR code');
const blob = await qrCodeInstance.getRawData('png') as Blob | null;
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(
qrImageBitmap,
labelCanvasCtx.canvas.width - qrSize - qrMarginRight,
(labelCanvasCtx.canvas.height - qrSize) / 2,
qrSize,
qrSize,
);
qrImageBitmap.close();
labelCanvasCtx.drawImage(
qrImageBitmap,
labelCanvasCtx.canvas.width - qrSize - qrMarginRight,
(labelCanvasCtx.canvas.height - qrSize) / 2,
qrSize,
qrSize,
);
qrImageBitmap.close();
} catch (err) {
// nop
}
}
const data = labelCanvasCtx.getImageData(0, 0, labelCanvasCtx.canvas.width, labelCanvasCtx.canvas.height);
@ -192,8 +198,10 @@ export class ImageLabelRenderer {
params: {
image: 'image',
label: 'label',
imageMarginX: paddingLeft / renderWidth,
imageMarginY: paddingTop / renderHeight,
paddingLeft: paddingLeft / renderWidth,
paddingRight: paddingRight / renderWidth,
paddingTop: paddingTop / renderHeight,
paddingBottom: paddingBottom / renderHeight,
},
}]);
}