From a54b4f2fd90d5563f5d65a522a21060da6757407 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sun, 2 Nov 2025 16:08:12 +0900 Subject: [PATCH] wip --- locales/index.d.ts | 8 ++ locales/ja-JP.yml | 2 + .../components/MkImageFrameEditorDialog.vue | 94 +++++++++++++----- .../src/utility/image-effector/fxs/frame.glsl | 16 ++- .../src/utility/image-effector/fxs/frame.ts | 25 +++-- .../src/utility/image-frame-renderer.ts | 99 +++++++++++-------- 6 files changed, 167 insertions(+), 77 deletions(-) diff --git a/locales/index.d.ts b/locales/index.d.ts index 7168abd5ca..e753cebeb2 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -5618,6 +5618,14 @@ export interface Locale extends ILocale { * 画像にフレームやメタデータを含んだラベルを追加して装飾できます。 */ "tip": string; + /** + * ヘッダー + */ + "header": string; + /** + * フッター + */ + "footer": string; /** * フチの幅 */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index c1da09f501..d8536aa4c4 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1401,6 +1401,8 @@ frame: "フレーム" _imageFrameEditor: title: "フレームの編集" tip: "画像にフレームやメタデータを含んだラベルを追加して装飾できます。" + header: "ヘッダー" + footer: "フッター" borderThickness: "フチの幅" labelThickness: "ラベルの幅" labelScale: "ラベルのスケール" diff --git a/packages/frontend/src/components/MkImageFrameEditorDialog.vue b/packages/frontend/src/components/MkImageFrameEditorDialog.vue index 85f53f43e5..dc4944db43 100644 --- a/packages/frontend/src/components/MkImageFrameEditorDialog.vue +++ b/packages/frontend/src/components/MkImageFrameEditorDialog.vue @@ -35,29 +35,65 @@ SPDX-License-Identifier: AGPL-3.0-only - - - + + - - - +
+ + + - - - + + + - - - + + + - - - + + + - - - + + + + + + + +
+
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+
{{ i18n.ts._imageFrameEditor.availableVariables }}:
@@ -118,12 +154,22 @@ const props = defineProps<{ const frame = reactive(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], }); diff --git a/packages/frontend/src/utility/image-effector/fxs/frame.glsl b/packages/frontend/src/utility/image-effector/fxs/frame.glsl index 5a260e4d9f..9030781151 100644 --- a/packages/frontend/src/utility/image-effector/fxs/frame.glsl +++ b/packages/frontend/src/utility/image-effector/fxs/frame.glsl @@ -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; diff --git a/packages/frontend/src/utility/image-effector/fxs/frame.ts b/packages/frontend/src/utility/image-effector/fxs/frame.ts index 79b7b66b07..89e9631517 100644 --- a/packages/frontend/src/utility/image-effector/fxs/frame.ts +++ b/packages/frontend/src/utility/image-effector/fxs/frame.ts @@ -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); } }, }); diff --git a/packages/frontend/src/utility/image-frame-renderer.ts b/packages/frontend/src/utility/image-frame-renderer.ts index bce3d12eb2..a261c8d58b 100644 --- a/packages/frontend/src/utility/image-frame-renderer.ts +++ b/packages/frontend/src/utility/image-frame-renderer.ts @@ -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[]; // 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 { - 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 { + 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,