diff options
| author | かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> | 2025-10-20 15:05:23 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-10-20 15:05:23 +0900 |
| commit | 8714945ec9deb88e1af6164b9290c9cf7e633aab (patch) | |
| tree | acd0db676544c524e7b6ace0c36ae51c376bb20f /packages | |
| parent | Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop (diff) | |
| download | misskey-8714945ec9deb88e1af6164b9290c9cf7e633aab.tar.gz misskey-8714945ec9deb88e1af6164b9290c9cf7e633aab.tar.bz2 misskey-8714945ec9deb88e1af6164b9290c9cf7e633aab.zip | |
fix(frontend): ウォーターマーク配置のエフェクトが壊れている問題を修正 (#16662)
* fix(frontend): ウォーターマーク配置のエフェクトが壊れている問題を修正
* enhance: add settings for noBoundingBoxExpansion
* Update Changelog
* fix
* perf: ウォーターマークのrepeatをWRAP属性で制御するように
* fix: ウォーターマークをrepeatした際に回転や拡大縮小の中心が「位置」設定を考慮しないのを修正
* fix: ウォーターマークをrepeatした際にマージンが各ウォーターマークごとのマージンとなっていない問題を修正
* fix: リピートモード時の拡大縮小の原点が、アライメントの設定にかかわらず左上になる問題を修正
* enhance: preserveBoundingRect の翻訳文字を変更
* fix: remove description
* fix
* fix: 回転の向きが逆になっているのを修正
* fix: マージンは元画像の大きさに対する割合で算出するように
* Update watermarkPlacement.ts
---------
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
Diffstat (limited to 'packages')
4 files changed, 187 insertions, 75 deletions
diff --git a/packages/frontend/src/components/MkWatermarkEditorDialog.Layer.vue b/packages/frontend/src/components/MkWatermarkEditorDialog.Layer.vue index 288293db3f..b34181e5cc 100644 --- a/packages/frontend/src/components/MkWatermarkEditorDialog.Layer.vue +++ b/packages/frontend/src/components/MkWatermarkEditorDialog.Layer.vue @@ -65,6 +65,10 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSwitch v-model="layer.repeat"> <template #label>{{ i18n.ts._watermarkEditor.repeat }}</template> </MkSwitch> + + <MkSwitch v-model="layerPreserveBoundingRect"> + <template #label>{{ i18n.ts._watermarkEditor.preserveBoundingRect }}</template> + </MkSwitch> </template> <template v-else-if="layer.type === 'image'"> @@ -129,6 +133,10 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSwitch v-model="layer.cover"> <template #label>{{ i18n.ts._watermarkEditor.cover }}</template> </MkSwitch> + + <MkSwitch v-model="layerPreserveBoundingRect"> + <template #label>{{ i18n.ts._watermarkEditor.preserveBoundingRect }}</template> + </MkSwitch> </template> <template v-else-if="layer.type === 'qr'"> @@ -335,7 +343,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script setup lang="ts"> -import { ref, onMounted } from 'vue'; +import { ref, onMounted, computed } from 'vue'; import * as Misskey from 'misskey-js'; import type { WatermarkPreset } from '@/utility/watermark.js'; import { i18n } from '@/i18n.js'; @@ -351,6 +359,20 @@ import { misskeyApi } from '@/utility/misskey-api.js'; const layer = defineModel<WatermarkPreset['layers'][number]>('layer', { required: true }); +const layerPreserveBoundingRect = computed({ + get: () => { + if (layer.value.type === 'text' || layer.value.type === 'image') { + return !layer.value.noBoundingBoxExpansion; + } + return false; + }, + set: (v: boolean) => { + if (layer.value.type === 'text' || layer.value.type === 'image') { + layer.value.noBoundingBoxExpansion = !v; + } + }, +}); + const driveFile = ref<Misskey.entities.DriveFile | null>(null); const driveFileError = ref(false); onMounted(async () => { diff --git a/packages/frontend/src/components/MkWatermarkEditorDialog.vue b/packages/frontend/src/components/MkWatermarkEditorDialog.vue index 0d0488d9bc..3b3f20d8d1 100644 --- a/packages/frontend/src/components/MkWatermarkEditorDialog.vue +++ b/packages/frontend/src/components/MkWatermarkEditorDialog.vue @@ -90,6 +90,7 @@ function createTextLayer(): WatermarkPreset['layers'][number] { angle: 0, opacity: 0.75, repeat: false, + noBoundingBoxExpansion: false, }; } @@ -104,6 +105,7 @@ function createImageLayer(): WatermarkPreset['layers'][number] { angle: 0, opacity: 0.75, repeat: false, + noBoundingBoxExpansion: false, cover: false, }; } diff --git a/packages/frontend/src/utility/image-effector/fxs/watermarkPlacement.ts b/packages/frontend/src/utility/image-effector/fxs/watermarkPlacement.ts index f79acb44b0..944e790792 100644 --- a/packages/frontend/src/utility/image-effector/fxs/watermarkPlacement.ts +++ b/packages/frontend/src/utility/image-effector/fxs/watermarkPlacement.ts @@ -9,80 +9,143 @@ const shader = `#version 300 es precision mediump float; const float PI = 3.141592653589793; -const float TWO_PI = 6.283185307179586; -const float HALF_PI = 1.5707963267948966; -in vec2 in_uv; -uniform sampler2D in_texture; -uniform vec2 in_resolution; -uniform sampler2D u_texture_watermark; -uniform vec2 u_resolution_watermark; -uniform float u_scale; -uniform float u_angle; -uniform float u_opacity; -uniform bool u_repeat; -uniform int u_alignX; // 0: left, 1: center, 2: right -uniform int u_alignY; // 0: top, 1: center, 2: bottom -uniform float u_alignMargin; -uniform int u_fitMode; // 0: contain, 1: cover +in vec2 in_uv; // 0..1 +uniform sampler2D in_texture; // 背景 +uniform vec2 in_resolution; // 出力解像度(px) + +uniform sampler2D u_watermark; // ウォーターマーク +uniform vec2 u_wmResolution; // ウォーターマーク元解像度(px) + +uniform float u_opacity; // 0..1 +uniform float u_scale; // watermarkのスケール +uniform float u_angle; // -1..1 (PI倍) +uniform bool u_cover; // cover基準 or fit基準 +uniform bool u_repeat; // タイル敷き詰め +uniform int u_alignX; // 0:left 1:center 2:right +uniform int u_alignY; // 0:top 1:center 2:bottom +uniform float u_margin; // 余白(比率) +uniform float u_repeatMargin; // 敷き詰め時の余白(比率) +uniform bool u_noBBoxExpansion; // 回転時のBounding Box拡張を抑止 +uniform bool u_wmEnabled; // watermark有効 + out vec4 out_color; +mat2 rot(float a) { + float c = cos(a), s = sin(a); + return mat2(c, -s, s, c); +} + +// cover/fitとscaleから、最終的なサイズ(px)を計算 +vec2 computeWmSize(vec2 outSize, vec2 wmSize, bool cover, float scale) { + float wmAspect = wmSize.x / wmSize.y; + float outAspect = outSize.x / outSize.y; + vec2 size; + if (cover) { + if (wmAspect >= outAspect) { + size.y = outSize.y * scale; + size.x = size.y * wmAspect; + } else { + size.x = outSize.x * scale; + size.y = size.x / wmAspect; + } + } else { + if (wmAspect >= outAspect) { + size.x = outSize.x * scale; + size.y = size.x / wmAspect; + } else { + size.y = outSize.y * scale; + size.x = size.y * wmAspect; + } + } + return size; +} + void main() { - vec4 in_color = texture(in_texture, in_uv); - float in_x_ratio = max(in_resolution.x / in_resolution.y, 1.0); - float in_y_ratio = max(in_resolution.y / in_resolution.x, 1.0); + vec2 outSize = in_resolution; + vec2 p = in_uv * outSize; // 出力のピクセル座標 + vec4 base = texture(in_texture, in_uv); + + if (!u_wmEnabled) { + out_color = base; + return; + } + + float theta = u_angle * PI; // ラジアン + vec2 wmSize = computeWmSize(outSize, u_wmResolution, u_cover, u_scale); + vec2 margin = u_repeat ? wmSize * u_repeatMargin : outSize * u_margin; + + // アライメントに基づく回転中心を計算 + float rotateX = 0.0; + float rotateY = 0.0; + if (abs(theta) > 1e-6 && !u_noBBoxExpansion) { + rotateX = abs(abs(wmSize.x * cos(theta)) + abs(wmSize.y * sin(theta)) - wmSize.x) * 0.5; + rotateY = abs(abs(wmSize.x * sin(theta)) + abs(wmSize.y * cos(theta)) - wmSize.y) * 0.5; + } + + float x; + if (u_alignX == 1) { + x = (outSize.x - wmSize.x) * 0.5; + } else if (u_alignX == 0) { + x = rotateX + margin.x; + } else { + x = outSize.x - wmSize.x - margin.x - rotateX; + } - bool contain = u_fitMode == 0; + float y; + if (u_alignY == 1) { + y = (outSize.y - wmSize.y) * 0.5; + } else if (u_alignY == 0) { + y = rotateY + margin.y; + } else { + y = outSize.y - wmSize.y - margin.y - rotateY; + } - float x_ratio = u_resolution_watermark.x / in_resolution.x; - float y_ratio = u_resolution_watermark.y / in_resolution.y; + vec2 rectMin = vec2(x, y); + vec2 rectMax = rectMin + wmSize; + vec2 rectCenter = (rectMin + rectMax) * 0.5; - float aspect_ratio = contain ? - (min(x_ratio, y_ratio) / max(x_ratio, y_ratio)) : - (max(x_ratio, y_ratio) / min(x_ratio, y_ratio)); + vec4 wmCol = vec4(0.0); - float x_scale = contain ? - (x_ratio > y_ratio ? 1.0 * u_scale : aspect_ratio * u_scale) : - (x_ratio > y_ratio ? aspect_ratio * u_scale : 1.0 * u_scale); + if (u_repeat) { + // アライメントに基づく中心で回転 + vec2 q = rectCenter + rot(theta) * (p - rectCenter); - float y_scale = contain ? - (y_ratio > x_ratio ? 1.0 * u_scale : aspect_ratio * u_scale) : - (y_ratio > x_ratio ? aspect_ratio * u_scale : 1.0 * u_scale); + // タイルグリッドの原点をrectMin(アライメント位置)に設定 + vec2 gridOrigin = rectMin - margin; + vec2 qFromOrigin = q - gridOrigin; - float x_offset = u_alignX == 0 ? x_scale / 2.0 : u_alignX == 2 ? 1.0 - (x_scale / 2.0) : 0.5; - float y_offset = u_alignY == 0 ? y_scale / 2.0 : u_alignY == 2 ? 1.0 - (y_scale / 2.0) : 0.5; + // タイルサイズ(ウォーターマーク + マージン)で正規化 + vec2 tile = wmSize + margin * 2.0; + vec2 tileUv = qFromOrigin / tile; - x_offset += (u_alignX == 0 ? 1.0 : u_alignX == 2 ? -1.0 : 0.0) * u_alignMargin; - y_offset += (u_alignY == 0 ? 1.0 : u_alignY == 2 ? -1.0 : 0.0) * u_alignMargin; + // タイル内のローカル座標(0..1)を取得 + vec2 localUv = fract(tileUv); - float angle = -(u_angle * PI); - vec2 center = vec2(x_offset, y_offset); - //vec2 centeredUv = (in_uv - center) * vec2(in_x_ratio, in_y_ratio); - vec2 centeredUv = (in_uv - center); - vec2 rotatedUV = vec2( - centeredUv.x * cos(angle) - centeredUv.y * sin(angle), - centeredUv.x * sin(angle) + centeredUv.y * cos(angle) - ) + center; + // ローカル座標をピクセル単位に変換 + vec2 localPos = localUv * tile; - // trim - if (!u_repeat) { - bool isInside = rotatedUV.x > x_offset - (x_scale / 2.0) && rotatedUV.x < x_offset + (x_scale / 2.0) && - rotatedUV.y > y_offset - (y_scale / 2.0) && rotatedUV.y < y_offset + (y_scale / 2.0); - if (!isInside) { - out_color = in_color; - return; + // マージン領域内かチェック + bool inMargin = any(lessThan(localPos, margin)) || any(greaterThanEqual(localPos, margin + wmSize)); + + if (!inMargin) { + // ウォーターマーク領域内: UV座標を計算 + vec2 uvWm = (localPos - margin) / wmSize; + wmCol = texture(u_watermark, uvWm); + } + // マージン領域の場合は透明(wmCol = vec4(0.0))のまま + } else { + // アライメントと回転に従い一枚だけ描画 + vec2 q = rectCenter + rot(theta) * (p - rectCenter); + bool inside = all(greaterThanEqual(q, rectMin)) && all(lessThan(q, rectMax)); + if (inside) { + vec2 uvWm = (q - rectMin) / wmSize; + wmCol = texture(u_watermark, uvWm); } } - vec4 watermark_color = texture(u_texture_watermark, vec2( - (rotatedUV.x - (x_offset - (x_scale / 2.0))) / x_scale, - (rotatedUV.y - (y_offset - (y_scale / 2.0))) / y_scale - )); - - out_color.r = mix(in_color.r, watermark_color.r, u_opacity * watermark_color.a); - out_color.g = mix(in_color.g, watermark_color.g, u_opacity * watermark_color.a); - out_color.b = mix(in_color.b, watermark_color.b, u_opacity * watermark_color.a); - out_color.a = in_color.a * (1.0 - u_opacity * watermark_color.a) + watermark_color.a * u_opacity; + float a = clamp(wmCol.a * u_opacity, 0.0, 1.0); + out_color = mix(base, vec4(wmCol.rgb, 1.0), a); } `; @@ -90,7 +153,7 @@ export const FX_watermarkPlacement = defineImageEffectorFx({ id: 'watermarkPlacement', name: '(internal)', shader, - uniforms: ['texture_watermark', 'resolution_watermark', 'scale', 'angle', 'opacity', 'repeat', 'alignX', 'alignY', 'alignMargin', 'fitMode'] as const, + uniforms: ['opacity', 'scale', 'angle', 'cover', 'repeat', 'alignX', 'alignY', 'margin', 'repeatMargin', 'noBBoxExpansion', 'wmResolution', 'wmEnabled', 'watermark'] as const, params: { cover: { type: 'boolean', @@ -125,29 +188,50 @@ export const FX_watermarkPlacement = defineImageEffectorFx({ max: 1.0, step: 0.01, }, + noBoundingBoxExpansion: { + type: 'boolean', + default: false, + }, watermark: { type: 'texture', default: null, }, }, main: ({ gl, u, params, textures }) => { - if (textures.watermark == null) { - return; - } + // 基本パラメータ + gl.uniform1f(u.opacity, params.opacity ?? 1.0); + gl.uniform1f(u.scale, params.scale ?? 0.3); + gl.uniform1f(u.angle, params.angle ?? 0.0); + gl.uniform1i(u.cover, params.cover ? 1 : 0); + gl.uniform1i(u.repeat, params.repeat ? 1 : 0); + const ax = params.align?.x === 'left' ? 0 : params.align?.x === 'center' ? 1 : 2; + const ay = params.align?.y === 'top' ? 0 : params.align?.y === 'center' ? 1 : 2; + gl.uniform1i(u.alignX, ax); + gl.uniform1i(u.alignY, ay); + gl.uniform1f(u.margin, (params.align?.margin ?? 0)); + gl.uniform1f(u.repeatMargin, (params.align?.margin ?? 0)); + gl.uniform1i(u.noBBoxExpansion, params.noBoundingBoxExpansion ? 1 : 0); - gl.activeTexture(gl.TEXTURE1); - gl.bindTexture(gl.TEXTURE_2D, textures.watermark.texture); - gl.uniform1i(u.texture_watermark, 1); + // ウォーターマークテクスチャ + const wm = textures.watermark; + if (wm) { + gl.activeTexture(gl.TEXTURE1); + gl.bindTexture(gl.TEXTURE_2D, wm.texture); - gl.uniform2fv(u.resolution_watermark, [textures.watermark.width, textures.watermark.height]); - gl.uniform1f(u.scale, params.scale); + // リピートモードに応じてWRAP属性を設定 + if (params.repeat) { + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); + } else { + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + } - gl.uniform1f(u.opacity, params.opacity); - gl.uniform1f(u.angle, params.angle); - gl.uniform1i(u.repeat, params.repeat ? 1 : 0); - gl.uniform1i(u.alignX, params.align.x === 'left' ? 0 : params.align.x === 'right' ? 2 : 1); - gl.uniform1i(u.alignY, params.align.y === 'top' ? 0 : params.align.y === 'bottom' ? 2 : 1); - gl.uniform1f(u.alignMargin, params.align.margin ?? 0); - gl.uniform1i(u.fitMode, params.cover ? 1 : 0); + gl.uniform1i(u.watermark, 1); + gl.uniform2f(u.wmResolution, wm.width, wm.height); + gl.uniform1i(u.wmEnabled, 1); + } else { + gl.uniform1i(u.wmEnabled, 0); + } }, }); diff --git a/packages/frontend/src/utility/watermark.ts b/packages/frontend/src/utility/watermark.ts index b3525f158f..1b46721a2b 100644 --- a/packages/frontend/src/utility/watermark.ts +++ b/packages/frontend/src/utility/watermark.ts @@ -27,6 +27,7 @@ export type WatermarkPreset = { type: 'text'; text: string; repeat: boolean; + noBoundingBoxExpansion: boolean; scale: number; angle: number; align: Align; @@ -38,6 +39,7 @@ export type WatermarkPreset = { imageId: string | null; cover: boolean; repeat: boolean; + noBoundingBoxExpansion: boolean; scale: number; angle: number; align: Align; @@ -106,6 +108,7 @@ export class WatermarkRenderer { id: layer.id, params: { repeat: layer.repeat, + noBoundingBoxExpansion: layer.noBoundingBoxExpansion, scale: layer.scale, align: layer.align, angle: layer.angle, @@ -123,6 +126,7 @@ export class WatermarkRenderer { id: layer.id, params: { repeat: layer.repeat, + noBoundingBoxExpansion: layer.noBoundingBoxExpansion, scale: layer.scale, align: layer.align, angle: layer.angle, |