summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorかっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>2025-10-20 15:05:23 +0900
committerGitHub <noreply@github.com>2025-10-20 15:05:23 +0900
commit8714945ec9deb88e1af6164b9290c9cf7e633aab (patch)
treeacd0db676544c524e7b6ace0c36ae51c376bb20f
parentMerge branch 'develop' of https://github.com/misskey-dev/misskey into develop (diff)
downloadmisskey-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>
-rw-r--r--CHANGELOG.md4
-rw-r--r--locales/index.d.ts4
-rw-r--r--locales/ja-JP.yml1
-rw-r--r--packages/frontend/src/components/MkWatermarkEditorDialog.Layer.vue24
-rw-r--r--packages/frontend/src/components/MkWatermarkEditorDialog.vue2
-rw-r--r--packages/frontend/src/utility/image-effector/fxs/watermarkPlacement.ts232
-rw-r--r--packages/frontend/src/utility/watermark.ts4
7 files changed, 196 insertions, 75 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3e024a3043..e74b950467 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,10 @@
- Fix: 一部のブラウザでバナー画像が上下中央に表示されない問題を修正
- Fix: ナビゲーションバーの設定で削除した項目をその場で再追加できない問題を修正
- Fix: ロールポリシーによりダイレクトメッセージが無効化されている際のデッキのダイレクトメッセージカラムの挙動を改善
+- Fix: ウォーターマークの各種挙動修正
+ - ウォーターマークを回転させると歪む問題を修正
+ - ウォーターマークを敷き詰めると上下左右反転した画像/文字が表示される問題を修正
+ - ウォーターマークを回転させた際に画面からはみ出た部分を考慮できるように
### Server
-
diff --git a/locales/index.d.ts b/locales/index.d.ts
index d79db121db..96d6c890a8 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -12346,6 +12346,10 @@ export interface Locale extends ILocale {
*/
"repeat": string;
/**
+ * 回転時はみ出ないように調整する
+ */
+ "preserveBoundingRect": string;
+ /**
* 不透明度
*/
"opacity": string;
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 5d1c37740c..8e935b5d9e 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -3305,6 +3305,7 @@ _watermarkEditor:
title: "ウォーターマークの編集"
cover: "全体に被せる"
repeat: "敷き詰める"
+ preserveBoundingRect: "回転時はみ出ないように調整する"
opacity: "不透明度"
scale: "サイズ"
text: "テキスト"
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,