diff --git a/locales/index.d.ts b/locales/index.d.ts index 17cdc7ca64..a4671aa812 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -12176,6 +12176,10 @@ export interface Locale extends ILocale { * 白黒 */ "grayscale": string; + /** + * 色調補正 + */ + "colorAdjust": string; /** * 色の圧縮 */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index ed1f5354df..4988bfc259 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -3262,6 +3262,7 @@ _imageEffector: mirror: "ミラー" invert: "色の反転" grayscale: "白黒" + colorAdjust: "色調補正" colorClamp: "色の圧縮" colorClampAdvanced: "色の圧縮(高度)" distort: "歪み" diff --git a/packages/frontend/src/utility/image-effector/fxs.ts b/packages/frontend/src/utility/image-effector/fxs.ts index 5887a68c43..a5c8e2ff80 100644 --- a/packages/frontend/src/utility/image-effector/fxs.ts +++ b/packages/frontend/src/utility/image-effector/fxs.ts @@ -5,6 +5,7 @@ import { FX_checker } from './fxs/checker.js'; import { FX_chromaticAberration } from './fxs/chromaticAberration.js'; +import { FX_colorAdjust } from './fxs/colorAdjust.js'; import { FX_colorClamp } from './fxs/colorClamp.js'; import { FX_colorClampAdvanced } from './fxs/colorClampAdvanced.js'; import { FX_distort } from './fxs/distort.js'; @@ -26,6 +27,7 @@ export const FXS = [ FX_mirror, FX_invert, FX_grayscale, + FX_colorAdjust, FX_colorClamp, FX_colorClampAdvanced, FX_distort, diff --git a/packages/frontend/src/utility/image-effector/fxs/colorAdjust.ts b/packages/frontend/src/utility/image-effector/fxs/colorAdjust.ts new file mode 100644 index 0000000000..cbb874852d --- /dev/null +++ b/packages/frontend/src/utility/image-effector/fxs/colorAdjust.ts @@ -0,0 +1,136 @@ +/* + * 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; + +in vec2 in_uv; +uniform sampler2D in_texture; +uniform vec2 in_resolution; +uniform float u_brightness; +uniform float u_contrast; +uniform float u_hue; +uniform float u_lightness; +uniform float u_saturation; +out vec4 out_color; + +// RGB to HSL +vec3 rgb2hsl(vec3 c) { + float maxc = max(max(c.r, c.g), c.b); + float minc = min(min(c.r, c.g), c.b); + float l = (maxc + minc) * 0.5; + float s = 0.0; + float h = 0.0; + if (maxc != minc) { + float d = maxc - minc; + s = l > 0.5 ? d / (2.0 - maxc - minc) : d / (maxc + minc); + if (maxc == c.r) { + h = (c.g - c.b) / d + (c.g < c.b ? 6.0 : 0.0); + } else if (maxc == c.g) { + h = (c.b - c.r) / d + 2.0; + } else { + h = (c.r - c.g) / d + 4.0; + } + h /= 6.0; + } + return vec3(h, s, l); +} + +// HSL to RGB +float hue2rgb(float p, float q, float t) { + if (t < 0.0) t += 1.0; + if (t > 1.0) t -= 1.0; + if (t < 1.0/6.0) return p + (q - p) * 6.0 * t; + if (t < 1.0/2.0) return q; + if (t < 2.0/3.0) return p + (q - p) * (2.0/3.0 - t) * 6.0; + return p; +} +vec3 hsl2rgb(vec3 hsl) { + float r, g, b; + float h = hsl.x; + float s = hsl.y; + float l = hsl.z; + if (s == 0.0) { + r = g = b = l; + } else { + float q = l < 0.5 ? l * (1.0 + s) : l + s - l * s; + float p = 2.0 * l - q; + r = hue2rgb(p, q, h + 1.0/3.0); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1.0/3.0); + } + return vec3(r, g, b); +} + +void main() { + vec4 in_color = texture(in_texture, in_uv); + vec3 color = in_color.rgb; + + color = color * u_brightness; + color += vec3(clamp(u_lightness, 0.0, 2.0) - 1.0); + color = (color - 0.5) * u_contrast + 0.5; + + vec3 hsl = rgb2hsl(color); + hsl.x = mod(hsl.x + u_hue, 1.0); + hsl.y = clamp(hsl.y * u_saturation, 0.0, 1.0); + + color = hsl2rgb(hsl); + out_color = vec4(color, in_color.a); +} +`; + +export const FX_colorAdjust = defineImageEffectorFx({ + id: 'colorAdjust' as const, + name: i18n.ts._imageEffector._fxs.colorAdjust, + shader, + uniforms: ['lightness', 'contrast', 'hue', 'brightness', 'saturation'] as const, + params: { + lightness: { + type: 'number' as const, + default: 100, + min: 0, + max: 200, + step: 1, + }, + contrast: { + type: 'number' as const, + default: 100, + min: 0, + max: 200, + step: 1, + }, + hue: { + type: 'number' as const, + default: 0, + min: -360, + max: 360, + step: 1, + }, + brightness: { + type: 'number' as const, + default: 100, + min: 0, + max: 200, + step: 1, + }, + saturation: { + type: 'number' as const, + default: 100, + min: 0, + max: 200, + step: 1, + }, + }, + main: ({ gl, u, params }) => { + gl.uniform1f(u.brightness, params.brightness / 100); + gl.uniform1f(u.contrast, params.contrast / 100); + gl.uniform1f(u.hue, params.hue / 360); + gl.uniform1f(u.lightness, params.lightness / 100); + gl.uniform1f(u.saturation, params.saturation / 100); + }, +});