summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md2
-rw-r--r--locales/index.d.ts4
-rw-r--r--locales/ja-JP.yml1
-rw-r--r--packages/frontend/src/components/MkImageEffectorDialog.vue20
-rw-r--r--packages/frontend/src/utility/image-effector/fxs.ts2
-rw-r--r--packages/frontend/src/utility/image-effector/fxs/pixelate.ts147
6 files changed, 174 insertions, 2 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2b721c04bb..fd64126227 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,7 +12,7 @@
- Feat: アカウントのQRコードを表示・読み取りできるようになりました
- Feat: 動画を圧縮してアップロードできるようになりました
- Enhance: チャットの日本語名称がダイレクトメッセージに戻るとともに、ベータ版機能ではなくなりました
-- Enhance: 画像編集にマスクエフェクト(塗りつぶし、ぼかし)を追加
+- Enhance: 画像編集にマスクエフェクト(塗りつぶし、ぼかし、モザイク)を追加
- Enhance: ウォーターマークにアカウントのQRコードを追加できるように
- Enhance: テーマをドラッグ&ドロップできるように
- Enhance: 絵文字ピッカーのサイズをより大きくできるように
diff --git a/locales/index.d.ts b/locales/index.d.ts
index 43e7d6e2a8..fbe8c30c94 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -12429,6 +12429,10 @@ export interface Locale extends ILocale {
*/
"blur": string;
/**
+ * モザイク
+ */
+ "pixelate": string;
+ /**
* 色調補正
*/
"colorAdjust": string;
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index e193abeb09..d21df5d5e1 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -3329,6 +3329,7 @@ _imageEffector:
invert: "色の反転"
grayscale: "白黒"
blur: "ぼかし"
+ pixelate: "モザイク"
colorAdjust: "色調補正"
colorClamp: "色の圧縮"
colorClampAdvanced: "色の圧縮(高度)"
diff --git a/packages/frontend/src/components/MkImageEffectorDialog.vue b/packages/frontend/src/components/MkImageEffectorDialog.vue
index 96fb01bb8c..5ce514f93e 100644
--- a/packages/frontend/src/components/MkImageEffectorDialog.vue
+++ b/packages/frontend/src/components/MkImageEffectorDialog.vue
@@ -216,7 +216,7 @@ watch(enabled, () => {
}
});
-const penMode = ref<'fill' | 'blur' | null>(null);
+const penMode = ref<'fill' | 'blur' | 'pixelate' | null>(null);
function showPenMenu(ev: MouseEvent) {
os.popupMenu([{
@@ -229,6 +229,11 @@ function showPenMenu(ev: MouseEvent) {
action: () => {
penMode.value = 'blur';
},
+ }, {
+ text: i18n.ts._imageEffector._fxs.pixelate,
+ action: () => {
+ penMode.value = 'pixelate';
+ },
}], ev.currentTarget ?? ev.target);
}
@@ -291,6 +296,19 @@ function onImagePointerdown(ev: PointerEvent) {
radius: 3,
},
});
+ } else if (penMode.value === 'pixelate') {
+ layers.push({
+ id,
+ fxId: 'pixelate',
+ params: {
+ offsetX: 0,
+ offsetY: 0,
+ scaleX: 0.1,
+ scaleY: 0.1,
+ angle: 0,
+ strength: 0.2,
+ },
+ });
}
_move(ev.offsetX, ev.offsetY);
diff --git a/packages/frontend/src/utility/image-effector/fxs.ts b/packages/frontend/src/utility/image-effector/fxs.ts
index 83ec20823d..2b20cc1f99 100644
--- a/packages/frontend/src/utility/image-effector/fxs.ts
+++ b/packages/frontend/src/utility/image-effector/fxs.ts
@@ -20,6 +20,7 @@ import { FX_zoomLines } from './fxs/zoomLines.js';
import { FX_blockNoise } from './fxs/blockNoise.js';
import { FX_fill } from './fxs/fill.js';
import { FX_blur } from './fxs/blur.js';
+import { FX_pixelate } from './fxs/pixelate.js';
import type { ImageEffectorFx } from './ImageEffector.js';
export const FXS = [
@@ -40,4 +41,5 @@ export const FXS = [
FX_blockNoise,
FX_fill,
FX_blur,
+ FX_pixelate,
] as const satisfies ImageEffectorFx<string, any>[];
diff --git a/packages/frontend/src/utility/image-effector/fxs/pixelate.ts b/packages/frontend/src/utility/image-effector/fxs/pixelate.ts
new file mode 100644
index 0000000000..d9a5f454f3
--- /dev/null
+++ b/packages/frontend/src/utility/image-effector/fxs/pixelate.ts
@@ -0,0 +1,147 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { defineImageEffectorFx } from '../ImageEffector.js';
+import { i18n } from '@/i18n.js';
+
+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 vec2 u_offset;
+uniform vec2 u_scale;
+uniform bool u_ellipse;
+uniform float u_angle;
+uniform int u_samples;
+uniform float u_strength;
+out vec4 out_color;
+
+// TODO: pixelateの中心を画像中心ではなく範囲の中心にする
+// TODO: 画像のアスペクト比に関わらず各画素は正方形にする
+
+void main() {
+ if (u_strength <= 0.0) {
+ out_color = texture(in_texture, in_uv);
+ return;
+ }
+
+ float angle = -(u_angle * PI);
+ vec2 centeredUv = in_uv - vec2(0.5, 0.5) - u_offset;
+ vec2 rotatedUV = vec2(
+ centeredUv.x * cos(angle) - centeredUv.y * sin(angle),
+ centeredUv.x * sin(angle) + centeredUv.y * cos(angle)
+ ) + u_offset;
+
+ bool isInside = false;
+ if (u_ellipse) {
+ vec2 norm = (rotatedUV - u_offset) / u_scale;
+ isInside = dot(norm, norm) <= 1.0;
+ } else {
+ isInside = rotatedUV.x > u_offset.x - u_scale.x && rotatedUV.x < u_offset.x + u_scale.x && rotatedUV.y > u_offset.y - u_scale.y && rotatedUV.y < u_offset.y + u_scale.y;
+ }
+
+ if (!isInside) {
+ out_color = texture(in_texture, in_uv);
+ return;
+ }
+
+ float dx = u_strength / 1.0;
+ float dy = u_strength / 1.0;
+ vec2 new_uv = vec2(
+ (dx * (floor((in_uv.x - 0.5 - (dx / 2.0)) / dx) + 0.5)),
+ (dy * (floor((in_uv.y - 0.5 - (dy / 2.0)) / dy) + 0.5))
+ ) + vec2(0.5 + (dx / 2.0), 0.5 + (dy / 2.0));
+
+ vec4 result = vec4(0.0);
+ float totalSamples = 0.0;
+
+ // TODO: より多くのサンプリング
+ result += texture(in_texture, new_uv);
+ totalSamples += 1.0;
+
+ out_color = totalSamples > 0.0 ? result / totalSamples : texture(in_texture, in_uv);
+}
+`;
+
+export const FX_pixelate = defineImageEffectorFx({
+ id: 'pixelate',
+ name: i18n.ts._imageEffector._fxs.pixelate,
+ shader,
+ uniforms: ['offset', 'scale', 'ellipse', 'angle', 'strength', 'samples'] as const,
+ params: {
+ offsetX: {
+ label: i18n.ts._imageEffector._fxProps.offset + ' X',
+ type: 'number',
+ default: 0.0,
+ min: -1.0,
+ max: 1.0,
+ step: 0.01,
+ toViewValue: v => Math.round(v * 100) + '%',
+ },
+ offsetY: {
+ label: i18n.ts._imageEffector._fxProps.offset + ' Y',
+ type: 'number',
+ default: 0.0,
+ min: -1.0,
+ max: 1.0,
+ step: 0.01,
+ toViewValue: v => Math.round(v * 100) + '%',
+ },
+ scaleX: {
+ label: i18n.ts._imageEffector._fxProps.scale + ' W',
+ type: 'number',
+ default: 0.5,
+ min: 0.0,
+ max: 1.0,
+ step: 0.01,
+ toViewValue: v => Math.round(v * 100) + '%',
+ },
+ scaleY: {
+ label: i18n.ts._imageEffector._fxProps.scale + ' H',
+ type: 'number',
+ default: 0.5,
+ min: 0.0,
+ max: 1.0,
+ step: 0.01,
+ toViewValue: v => Math.round(v * 100) + '%',
+ },
+ ellipse: {
+ label: i18n.ts._imageEffector._fxProps.circle,
+ type: 'boolean',
+ default: false,
+ },
+ angle: {
+ label: i18n.ts._imageEffector._fxProps.angle,
+ type: 'number',
+ default: 0,
+ min: -1.0,
+ max: 1.0,
+ step: 0.01,
+ toViewValue: v => Math.round(v * 90) + '°',
+ },
+ strength: {
+ label: i18n.ts._imageEffector._fxProps.strength,
+ type: 'number',
+ default: 0.2,
+ min: 0.0,
+ max: 0.5,
+ step: 0.01,
+ },
+ },
+ main: ({ gl, u, params }) => {
+ gl.uniform2f(u.offset, params.offsetX / 2, params.offsetY / 2);
+ gl.uniform2f(u.scale, params.scaleX / 2, params.scaleY / 2);
+ gl.uniform1i(u.ellipse, params.ellipse ? 1 : 0);
+ gl.uniform1f(u.angle, params.angle / 2);
+ gl.uniform1f(u.strength, params.strength * params.strength);
+ gl.uniform1i(u.samples, 256);
+ },
+});