wip
This commit is contained in:
parent
7bb9d3d0af
commit
a54b4f2fd9
|
|
@ -5618,6 +5618,14 @@ export interface Locale extends ILocale {
|
|||
* 画像にフレームやメタデータを含んだラベルを追加して装飾できます。
|
||||
*/
|
||||
"tip": string;
|
||||
/**
|
||||
* ヘッダー
|
||||
*/
|
||||
"header": string;
|
||||
/**
|
||||
* フッター
|
||||
*/
|
||||
"footer": string;
|
||||
/**
|
||||
* フチの幅
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1401,6 +1401,8 @@ frame: "フレーム"
|
|||
_imageFrameEditor:
|
||||
title: "フレームの編集"
|
||||
tip: "画像にフレームやメタデータを含んだラベルを追加して装飾できます。"
|
||||
header: "ヘッダー"
|
||||
footer: "フッター"
|
||||
borderThickness: "フチの幅"
|
||||
labelThickness: "ラベルの幅"
|
||||
labelScale: "ラベルのスケール"
|
||||
|
|
|
|||
|
|
@ -35,29 +35,65 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #label>{{ i18n.ts._imageFrameEditor.borderThickness }}</template>
|
||||
</MkRange>
|
||||
|
||||
<MkRange v-model="frame.labelThickness" :min="0.01" :max="0.5" :step="0.01" :continuousUpdate="true">
|
||||
<template #label>{{ i18n.ts._imageFrameEditor.labelThickness }}</template>
|
||||
</MkRange>
|
||||
<MkFolder :defaultOpen="true">
|
||||
<template #label>{{ i18n.ts._imageFrameEditor.header }}</template>
|
||||
|
||||
<MkRange v-model="frame.labelScale" :min="0.5" :max="2.0" :step="0.01" :continuousUpdate="true">
|
||||
<template #label>{{ i18n.ts._imageFrameEditor.labelScale }}</template>
|
||||
</MkRange>
|
||||
<div class="_gaps">
|
||||
<MkRange v-model="frame.labelTop.padding" :min="0.01" :max="0.5" :step="0.01" :continuousUpdate="true">
|
||||
<template #label>{{ i18n.ts._imageFrameEditor.labelThickness }}</template>
|
||||
</MkRange>
|
||||
|
||||
<MkSwitch v-model="frame.centered">
|
||||
<template #label>{{ i18n.ts._imageFrameEditor.centered }}</template>
|
||||
</MkSwitch>
|
||||
<MkRange v-model="frame.labelTop.scale" :min="0.5" :max="2.0" :step="0.01" :continuousUpdate="true">
|
||||
<template #label>{{ i18n.ts._imageFrameEditor.labelScale }}</template>
|
||||
</MkRange>
|
||||
|
||||
<MkInput v-model="frame.title">
|
||||
<template #label>{{ i18n.ts._imageFrameEditor.captionMain }}</template>
|
||||
</MkInput>
|
||||
<MkSwitch v-model="frame.labelTop.centered">
|
||||
<template #label>{{ i18n.ts._imageFrameEditor.centered }}</template>
|
||||
</MkSwitch>
|
||||
|
||||
<MkTextarea v-model="frame.text">
|
||||
<template #label>{{ i18n.ts._imageFrameEditor.captionSub }}</template>
|
||||
</MkTextarea>
|
||||
<MkInput v-model="frame.labelTop.textBig">
|
||||
<template #label>{{ i18n.ts._imageFrameEditor.captionMain }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkSwitch v-model="frame.withQrCode">
|
||||
<template #label>{{ i18n.ts._imageFrameEditor.withQrCode }}</template>
|
||||
</MkSwitch>
|
||||
<MkTextarea v-model="frame.labelTop.textSmall">
|
||||
<template #label>{{ i18n.ts._imageFrameEditor.captionSub }}</template>
|
||||
</MkTextarea>
|
||||
|
||||
<MkSwitch v-model="frame.labelTop.withQrCode">
|
||||
<template #label>{{ i18n.ts._imageFrameEditor.withQrCode }}</template>
|
||||
</MkSwitch>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder :defaultOpen="true">
|
||||
<template #label>{{ i18n.ts._imageFrameEditor.footer }}</template>
|
||||
|
||||
<div class="_gaps">
|
||||
<MkRange v-model="frame.labelBottom.padding" :min="0.01" :max="0.5" :step="0.01" :continuousUpdate="true">
|
||||
<template #label>{{ i18n.ts._imageFrameEditor.labelThickness }}</template>
|
||||
</MkRange>
|
||||
|
||||
<MkRange v-model="frame.labelBottom.scale" :min="0.5" :max="2.0" :step="0.01" :continuousUpdate="true">
|
||||
<template #label>{{ i18n.ts._imageFrameEditor.labelScale }}</template>
|
||||
</MkRange>
|
||||
|
||||
<MkSwitch v-model="frame.labelBottom.centered">
|
||||
<template #label>{{ i18n.ts._imageFrameEditor.centered }}</template>
|
||||
</MkSwitch>
|
||||
|
||||
<MkInput v-model="frame.labelBottom.textBig">
|
||||
<template #label>{{ i18n.ts._imageFrameEditor.captionMain }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkTextarea v-model="frame.labelBottom.textSmall">
|
||||
<template #label>{{ i18n.ts._imageFrameEditor.captionSub }}</template>
|
||||
</MkTextarea>
|
||||
|
||||
<MkSwitch v-model="frame.labelBottom.withQrCode">
|
||||
<template #label>{{ i18n.ts._imageFrameEditor.withQrCode }}</template>
|
||||
</MkSwitch>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkInfo>
|
||||
<div>{{ i18n.ts._imageFrameEditor.availableVariables }}:</div>
|
||||
|
|
@ -118,12 +154,22 @@ const props = defineProps<{
|
|||
|
||||
const frame = reactive<ImageFrameParams>(deepClone(props.frame) ?? {
|
||||
borderThickness: 0.05,
|
||||
labelThickness: 0.2,
|
||||
labelScale: 1.0,
|
||||
title: '{year}/{0month}/{0day}',
|
||||
text: '{camera_mm}mm f/{camera_f} {camera_s}s ISO{camera_iso}',
|
||||
centered: false,
|
||||
withQrCode: true,
|
||||
labelTop: {
|
||||
scale: 1.0,
|
||||
padding: 0.2,
|
||||
textBig: '',
|
||||
textSmall: '',
|
||||
centered: false,
|
||||
withQrCode: false,
|
||||
},
|
||||
labelBottom: {
|
||||
scale: 1.0,
|
||||
padding: 0.2,
|
||||
textBig: '{year}/{0month}/{0day}',
|
||||
textSmall: '{camera_mm}mm f/{camera_f} {camera_s}s ISO{camera_iso}',
|
||||
centered: false,
|
||||
withQrCode: true,
|
||||
},
|
||||
bgColor: [255, 255, 255],
|
||||
fgColor: [0, 0, 0],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ in vec2 in_uv;
|
|||
uniform sampler2D in_texture;
|
||||
uniform vec2 in_resolution;
|
||||
uniform sampler2D u_image;
|
||||
uniform sampler2D u_label;
|
||||
uniform sampler2D u_topLabel;
|
||||
uniform sampler2D u_bottomLabel;
|
||||
uniform float u_paddingTop;
|
||||
uniform float u_paddingBottom;
|
||||
uniform float u_paddingLeft;
|
||||
|
|
@ -27,13 +28,20 @@ void main() {
|
|||
remap(in_uv.y, u_paddingTop, 1.0 - u_paddingBottom, 0.0, 1.0)
|
||||
));
|
||||
|
||||
vec4 label_color = texture(u_label, vec2(
|
||||
vec4 topLabel_color = texture(u_topLabel, vec2(
|
||||
in_uv.x,
|
||||
remap(in_uv.y, 0.0, u_paddingTop, 0.0, 1.0)
|
||||
));
|
||||
|
||||
vec4 bottomLabel_color = texture(u_bottomLabel, vec2(
|
||||
in_uv.x,
|
||||
remap(in_uv.y, 1.0 - u_paddingBottom, 1.0, 0.0, 1.0)
|
||||
));
|
||||
|
||||
if (in_uv.y > (1.0 - u_paddingBottom)) {
|
||||
out_color = label_color;
|
||||
if (in_uv.y < u_paddingTop) {
|
||||
out_color = topLabel_color;
|
||||
} else if (in_uv.y > (1.0 - u_paddingBottom)) {
|
||||
out_color = bottomLabel_color;
|
||||
} else {
|
||||
if (in_uv.y > u_paddingTop && in_uv.x > u_paddingLeft && in_uv.x < (1.0 - u_paddingRight)) {
|
||||
out_color = image_color;
|
||||
|
|
|
|||
|
|
@ -10,13 +10,17 @@ export const FX_frame = defineImageEffectorFx({
|
|||
id: 'frame',
|
||||
name: '(internal)',
|
||||
shader,
|
||||
uniforms: ['image', 'label', 'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight'] as const,
|
||||
uniforms: ['image', 'topLabel', 'bottomLabel', 'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight'] as const,
|
||||
params: {
|
||||
image: {
|
||||
type: 'textureRef',
|
||||
default: null,
|
||||
},
|
||||
label: {
|
||||
topLabel: {
|
||||
type: 'textureRef',
|
||||
default: null,
|
||||
},
|
||||
bottomLabel: {
|
||||
type: 'textureRef',
|
||||
default: null,
|
||||
},
|
||||
|
|
@ -47,6 +51,8 @@ export const FX_frame = defineImageEffectorFx({
|
|||
},
|
||||
main: ({ gl, u, params, textures }) => {
|
||||
const image = textures.image;
|
||||
if (image == null) return;
|
||||
|
||||
gl.activeTexture(gl.TEXTURE1);
|
||||
gl.bindTexture(gl.TEXTURE_2D, image.texture);
|
||||
gl.uniform1i(u.image, 1);
|
||||
|
|
@ -56,11 +62,18 @@ export const FX_frame = defineImageEffectorFx({
|
|||
gl.uniform1f(u.paddingLeft, params.paddingLeft);
|
||||
gl.uniform1f(u.paddingRight, params.paddingRight);
|
||||
|
||||
const label = textures.label;
|
||||
if (label) {
|
||||
const topLabel = textures.topLabel;
|
||||
if (topLabel) {
|
||||
gl.activeTexture(gl.TEXTURE2);
|
||||
gl.bindTexture(gl.TEXTURE_2D, label.texture);
|
||||
gl.uniform1i(u.label, 2);
|
||||
gl.bindTexture(gl.TEXTURE_2D, topLabel.texture);
|
||||
gl.uniform1i(u.topLabel, 2);
|
||||
}
|
||||
|
||||
const bottomLabel = textures.bottomLabel;
|
||||
if (bottomLabel) {
|
||||
gl.activeTexture(gl.TEXTURE3);
|
||||
gl.bindTexture(gl.TEXTURE_2D, bottomLabel.texture);
|
||||
gl.uniform1i(u.bottomLabel, 3);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,20 +11,27 @@ import type { ImageEffectorFx, ImageEffectorLayer } from '@/utility/image-effect
|
|||
import { ImageEffector } from '@/utility/image-effector/ImageEffector.js';
|
||||
import { ensureSignin } from '@/i.js';
|
||||
|
||||
const $i = ensureSignin();
|
||||
|
||||
const FXS = [
|
||||
FX_frame,
|
||||
] as const satisfies ImageEffectorFx<string, any>[];
|
||||
|
||||
// TODO: 上部にもラベルを配置できるようにする
|
||||
|
||||
export type ImageFrameParams = {
|
||||
borderThickness: number;
|
||||
labelThickness: number;
|
||||
labelScale: number;
|
||||
title: string;
|
||||
text: string;
|
||||
type LabelParams = {
|
||||
scale: number;
|
||||
padding: number;
|
||||
textBig: string;
|
||||
textSmall: string;
|
||||
centered: boolean;
|
||||
withQrCode: boolean;
|
||||
};
|
||||
|
||||
export type ImageFrameParams = {
|
||||
borderThickness: number;
|
||||
labelTop: LabelParams;
|
||||
labelBottom: LabelParams;
|
||||
bgColor: [r: number, g: number, b: number];
|
||||
fgColor: [r: number, g: number, b: number];
|
||||
borderRadius: number; // TODO
|
||||
|
|
@ -64,7 +71,7 @@ export class ImageFrameRenderer {
|
|||
this.effector.registerTexture('image', this.image);
|
||||
}
|
||||
|
||||
private interpolateText(text: string) {
|
||||
private interpolateTemplateText(text: string) {
|
||||
return text.replaceAll(/\{(\w+)\}/g, (_: string, key: string) => {
|
||||
const meta_date = this.exif.DateTimeOriginal ? this.exif.DateTimeOriginal.description : '-';
|
||||
const date = meta_date.split(' ')[0].replaceAll(':', '/');
|
||||
|
|
@ -92,33 +99,11 @@ export class ImageFrameRenderer {
|
|||
});
|
||||
}
|
||||
|
||||
public async updateAndRender(params: ImageFrameParams): Promise<void> {
|
||||
let imageAreaW = this.image.width;
|
||||
let imageAreaH = this.image.height;
|
||||
|
||||
if (this.renderAsPreview) {
|
||||
const MAX_W = 1000;
|
||||
const MAX_H = 1000;
|
||||
|
||||
if (imageAreaW > MAX_W || imageAreaH > MAX_H) {
|
||||
const scale = Math.min(MAX_W / imageAreaW, MAX_H / imageAreaH);
|
||||
imageAreaW = Math.floor(imageAreaW * scale);
|
||||
imageAreaH = Math.floor(imageAreaH * scale);
|
||||
}
|
||||
}
|
||||
|
||||
const paddingTop = Math.floor(imageAreaH * params.borderThickness);
|
||||
const paddingLeft = Math.floor(imageAreaH * params.borderThickness);
|
||||
const paddingRight = Math.floor(imageAreaH * params.borderThickness);
|
||||
const paddingBottom = Math.floor(imageAreaH * params.labelThickness);
|
||||
const renderWidth = imageAreaW + paddingLeft + paddingRight;
|
||||
const renderHeight = imageAreaH + paddingTop + paddingBottom;
|
||||
|
||||
const aspectRatio = renderWidth / renderHeight;
|
||||
private async renderLabel(renderWidth: number, renderHeight: number, paddingLeft: number, paddingRight: number, imageAreaH: number, params: LabelParams) {
|
||||
const scaleBase = imageAreaH * params.scale;
|
||||
const labelCanvasCtx = window.document.createElement('canvas').getContext('2d')!;
|
||||
labelCanvasCtx.canvas.width = renderWidth;
|
||||
labelCanvasCtx.canvas.height = paddingBottom;
|
||||
const scaleBase = imageAreaH * params.labelScale;
|
||||
labelCanvasCtx.canvas.height = renderHeight;
|
||||
const fontSize = scaleBase / 30;
|
||||
const textsMarginLeft = Math.max(fontSize * 2, paddingLeft);
|
||||
const textsMarginRight = textsMarginLeft;
|
||||
|
|
@ -133,30 +118,28 @@ export class ImageFrameRenderer {
|
|||
labelCanvasCtx.font = `bold ${fontSize}px sans-serif`;
|
||||
labelCanvasCtx.textBaseline = 'middle';
|
||||
|
||||
const titleY = params.text === '' ? (labelCanvasCtx.canvas.height / 2) : (labelCanvasCtx.canvas.height / 2) - (fontSize * 0.9);
|
||||
const titleY = params.textSmall === '' ? (labelCanvasCtx.canvas.height / 2) : (labelCanvasCtx.canvas.height / 2) - (fontSize * 0.9);
|
||||
if (params.centered) {
|
||||
labelCanvasCtx.textAlign = 'center';
|
||||
labelCanvasCtx.fillText(this.interpolateText(params.title), labelCanvasCtx.canvas.width / 2, titleY, labelCanvasCtx.canvas.width - textsMarginLeft - textsMarginRight);
|
||||
labelCanvasCtx.fillText(this.interpolateTemplateText(params.textBig), labelCanvasCtx.canvas.width / 2, titleY, labelCanvasCtx.canvas.width - textsMarginLeft - textsMarginRight);
|
||||
} else {
|
||||
labelCanvasCtx.textAlign = 'left';
|
||||
labelCanvasCtx.fillText(this.interpolateText(params.title), textsMarginLeft, titleY, labelCanvasCtx.canvas.width - textsMarginLeft - (withQrCode ? (qrSize + qrMarginRight + (fontSize * 1)) : textsMarginRight));
|
||||
labelCanvasCtx.fillText(this.interpolateTemplateText(params.textBig), textsMarginLeft, titleY, labelCanvasCtx.canvas.width - textsMarginLeft - (withQrCode ? (qrSize + qrMarginRight + (fontSize * 1)) : textsMarginRight));
|
||||
}
|
||||
|
||||
labelCanvasCtx.fillStyle = '#00000088';
|
||||
labelCanvasCtx.font = `${fontSize * 0.85}px sans-serif`;
|
||||
labelCanvasCtx.textBaseline = 'middle';
|
||||
|
||||
const textY = params.title === '' ? (labelCanvasCtx.canvas.height / 2) : (labelCanvasCtx.canvas.height / 2) + (fontSize * 0.9);
|
||||
const textY = params.textBig === '' ? (labelCanvasCtx.canvas.height / 2) : (labelCanvasCtx.canvas.height / 2) + (fontSize * 0.9);
|
||||
if (params.centered) {
|
||||
labelCanvasCtx.textAlign = 'center';
|
||||
labelCanvasCtx.fillText(this.interpolateText(params.text), labelCanvasCtx.canvas.width / 2, textY, labelCanvasCtx.canvas.width - textsMarginLeft - textsMarginRight);
|
||||
labelCanvasCtx.fillText(this.interpolateTemplateText(params.textSmall), labelCanvasCtx.canvas.width / 2, textY, labelCanvasCtx.canvas.width - textsMarginLeft - textsMarginRight);
|
||||
} else {
|
||||
labelCanvasCtx.textAlign = 'left';
|
||||
labelCanvasCtx.fillText(this.interpolateText(params.text), textsMarginLeft, textY, labelCanvasCtx.canvas.width - textsMarginLeft - (withQrCode ? (qrSize + qrMarginRight + (fontSize * 1)) : textsMarginRight));
|
||||
labelCanvasCtx.fillText(this.interpolateTemplateText(params.textSmall), textsMarginLeft, textY, labelCanvasCtx.canvas.width - textsMarginLeft - (withQrCode ? (qrSize + qrMarginRight + (fontSize * 1)) : textsMarginRight));
|
||||
}
|
||||
|
||||
const $i = ensureSignin();
|
||||
|
||||
if (withQrCode) {
|
||||
try {
|
||||
const qrCodeInstance = new QRCodeStyling({
|
||||
|
|
@ -207,9 +190,38 @@ export class ImageFrameRenderer {
|
|||
}
|
||||
}
|
||||
|
||||
const data = labelCanvasCtx.getImageData(0, 0, labelCanvasCtx.canvas.width, labelCanvasCtx.canvas.height);
|
||||
return labelCanvasCtx;
|
||||
}
|
||||
|
||||
await this.effector.registerTexture('label', data);
|
||||
public async updateAndRender(params: ImageFrameParams): Promise<void> {
|
||||
let imageAreaW = this.image.width;
|
||||
let imageAreaH = this.image.height;
|
||||
|
||||
if (this.renderAsPreview) {
|
||||
const MAX_W = 1000;
|
||||
const MAX_H = 1000;
|
||||
|
||||
if (imageAreaW > MAX_W || imageAreaH > MAX_H) {
|
||||
const scale = Math.min(MAX_W / imageAreaW, MAX_H / imageAreaH);
|
||||
imageAreaW = Math.floor(imageAreaW * scale);
|
||||
imageAreaH = Math.floor(imageAreaH * scale);
|
||||
}
|
||||
}
|
||||
|
||||
const paddingLeft = Math.floor(imageAreaH * params.borderThickness);
|
||||
const paddingRight = Math.floor(imageAreaH * params.borderThickness);
|
||||
const paddingTop = Math.floor(imageAreaH * params.labelTop.padding);
|
||||
const paddingBottom = Math.floor(imageAreaH * params.labelBottom.padding);
|
||||
const renderWidth = imageAreaW + paddingLeft + paddingRight;
|
||||
const renderHeight = imageAreaH + paddingTop + paddingBottom;
|
||||
|
||||
const topLabelCanvasCtx = await this.renderLabel(renderWidth, paddingBottom, paddingLeft, paddingRight, imageAreaH, params.labelTop);
|
||||
const topLabelImage = topLabelCanvasCtx.getImageData(0, 0, topLabelCanvasCtx.canvas.width, topLabelCanvasCtx.canvas.height);
|
||||
this.effector.registerTexture('topLabel', topLabelImage);
|
||||
|
||||
const bottomLabelCanvasCtx = await this.renderLabel(renderWidth, paddingBottom, paddingLeft, paddingRight, imageAreaH, params.labelBottom);
|
||||
const bottomLabelImage = bottomLabelCanvasCtx.getImageData(0, 0, bottomLabelCanvasCtx.canvas.width, bottomLabelCanvasCtx.canvas.height);
|
||||
this.effector.registerTexture('bottomLabel', bottomLabelImage);
|
||||
|
||||
this.effector.changeResolution(renderWidth, renderHeight);
|
||||
|
||||
|
|
@ -218,7 +230,8 @@ export class ImageFrameRenderer {
|
|||
id: 'a',
|
||||
params: {
|
||||
image: 'image',
|
||||
label: 'label',
|
||||
topLabel: 'topLabel',
|
||||
bottomLabel: 'bottomLabel',
|
||||
paddingLeft: paddingLeft / renderWidth,
|
||||
paddingRight: paddingRight / renderWidth,
|
||||
paddingTop: paddingTop / renderHeight,
|
||||
|
|
|
|||
Loading…
Reference in New Issue