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/frontend/src/utility/image-effector | |
| 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/frontend/src/utility/image-effector')
| -rw-r--r-- | packages/frontend/src/utility/image-effector/fxs/watermarkPlacement.ts | 232 |
1 files changed, 158 insertions, 74 deletions
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); + } }, }); |