diff options
| author | syuilo <4439005+syuilo@users.noreply.github.com> | 2025-11-06 20:25:17 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-11-06 20:25:17 +0900 |
| commit | 4ba18690d7abd7eea086bb59e6cbcc8ead9e121a (patch) | |
| tree | 7d25ec47d8711d945b08e3903642f2e982f40048 /packages/frontend/src/utility/image-compositor-functions | |
| parent | fix(frontend): improve startViewTransition handling (diff) | |
| download | misskey-4ba18690d7abd7eea086bb59e6cbcc8ead9e121a.tar.gz misskey-4ba18690d7abd7eea086bb59e6cbcc8ead9e121a.tar.bz2 misskey-4ba18690d7abd7eea086bb59e6cbcc8ead9e121a.zip | |
feat(frontend): EXIFフレーム機能 (#16725)
* wip
* wip
* Update ImageEffector.ts
* Update image-label-renderer.ts
* Update image-label-renderer.ts
* wip
* Update image-label-renderer.ts
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* Update use-uploader.ts
* Update watermark.ts
* wip
* wu
* wip
* Update image-frame-renderer.ts
* wip
* wip
* Update image-frame-renderer.ts
* Create ImageCompositor.ts
* Update ImageCompositor.ts
* wip
* wip
* Update ImageEffector.ts
* wip
* Update use-uploader.ts
* wip
* wip
* wip
* wip
* Update fxs.ts
* wip
* wip
* wip
* Update CHANGELOG.md
* wip
* wip
* Update MkImageEffectorDialog.vue
* Update MkImageEffectorDialog.vue
* Update MkImageFrameEditorDialog.vue
* Update use-uploader.ts
* improve error handling
* Update use-uploader.ts
* 🎨
* wip
* wip
* lazy load
* lazy load
* wip
* wip
* wip
Diffstat (limited to 'packages/frontend/src/utility/image-compositor-functions')
35 files changed, 2033 insertions, 0 deletions
diff --git a/packages/frontend/src/utility/image-compositor-functions/blockNoise.glsl b/packages/frontend/src/utility/image-compositor-functions/blockNoise.glsl new file mode 100644 index 0000000000..84c4ecbed4 --- /dev/null +++ b/packages/frontend/src/utility/image-compositor-functions/blockNoise.glsl @@ -0,0 +1,43 @@ +#version 300 es +precision mediump float; + +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +in vec2 in_uv; +uniform sampler2D in_texture; +uniform vec2 in_resolution; +uniform int u_amount; +uniform float u_shiftStrengths[128]; +uniform vec2 u_shiftOrigins[128]; +uniform vec2 u_shiftSizes[128]; +uniform float u_channelShift; +out vec4 out_color; + +void main() { + // TODO: ピクセル毎に計算する必要はないのでuniformにする + float aspect_ratio = min(in_resolution.x, in_resolution.y) / max(in_resolution.x, in_resolution.y); + float aspect_ratio_x = in_resolution.x > in_resolution.y ? 1.0 : aspect_ratio; + float aspect_ratio_y = in_resolution.x < in_resolution.y ? 1.0 : aspect_ratio; + + float v = 0.0; + + for (int i = 0; i < u_amount; i++) { + if ( + in_uv.x * aspect_ratio_x > ((u_shiftOrigins[i].x * aspect_ratio_x) - u_shiftSizes[i].x) && + in_uv.x * aspect_ratio_x < ((u_shiftOrigins[i].x * aspect_ratio_x) + u_shiftSizes[i].x) && + in_uv.y * aspect_ratio_y > ((u_shiftOrigins[i].y * aspect_ratio_y) - u_shiftSizes[i].y) && + in_uv.y * aspect_ratio_y < ((u_shiftOrigins[i].y * aspect_ratio_y) + u_shiftSizes[i].y) + ) { + v += u_shiftStrengths[i]; + } + } + + float r = texture(in_texture, vec2(in_uv.x + (v * (1.0 + u_channelShift)), in_uv.y)).r; + float g = texture(in_texture, vec2(in_uv.x + v, in_uv.y)).g; + float b = texture(in_texture, vec2(in_uv.x + (v * (1.0 + (u_channelShift / 2.0))), in_uv.y)).b; + float a = texture(in_texture, vec2(in_uv.x + v, in_uv.y)).a; + out_color = vec4(r, g, b, a); +} diff --git a/packages/frontend/src/utility/image-compositor-functions/blockNoise.ts b/packages/frontend/src/utility/image-compositor-functions/blockNoise.ts new file mode 100644 index 0000000000..8c83ef51a0 --- /dev/null +++ b/packages/frontend/src/utility/image-compositor-functions/blockNoise.ts @@ -0,0 +1,95 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import seedrandom from 'seedrandom'; +import shader from './blockNoise.glsl'; +import type { ImageEffectorUiDefinition } from '../image-effector/ImageEffector.js'; +import { defineImageCompositorFunction } from '@/lib/ImageCompositor.js'; +import { i18n } from '@/i18n.js'; + +export const fn = defineImageCompositorFunction<{ + amount: number; + strength: number; + width: number; + height: number; + channelShift: number; + seed: number; +}>({ + shader, + main: ({ gl, program, u, params }) => { + gl.uniform1i(u.amount, params.amount); + gl.uniform1f(u.channelShift, params.channelShift); + + const margin = 0; + + const rnd = seedrandom(params.seed.toString()); + + for (let i = 0; i < params.amount; i++) { + const o = gl.getUniformLocation(program, `u_shiftOrigins[${i.toString()}]`); + gl.uniform2f(o, (rnd() * (1 + (margin * 2))) - margin, (rnd() * (1 + (margin * 2))) - margin); + + const s = gl.getUniformLocation(program, `u_shiftStrengths[${i.toString()}]`); + gl.uniform1f(s, (1 - (rnd() * 2)) * params.strength); + + const sizes = gl.getUniformLocation(program, `u_shiftSizes[${i.toString()}]`); + gl.uniform2f(sizes, params.width, params.height); + } + }, +}); + +export const uiDefinition = { + name: i18n.ts._imageEffector._fxs.glitch + ': ' + i18n.ts._imageEffector._fxs.blockNoise, + params: { + amount: { + label: i18n.ts._imageEffector._fxProps.amount, + type: 'number', + default: 50, + min: 1, + max: 100, + step: 1, + }, + strength: { + label: i18n.ts._imageEffector._fxProps.strength, + type: 'number', + default: 0.05, + min: -1, + max: 1, + step: 0.01, + toViewValue: v => Math.round(v * 100) + '%', + }, + width: { + label: i18n.ts.width, + type: 'number', + default: 0.05, + min: 0.01, + max: 1, + step: 0.01, + toViewValue: v => Math.round(v * 100) + '%', + }, + height: { + label: i18n.ts.height, + type: 'number', + default: 0.01, + min: 0.01, + max: 1, + step: 0.01, + toViewValue: v => Math.round(v * 100) + '%', + }, + channelShift: { + label: i18n.ts._imageEffector._fxProps.glitchChannelShift, + type: 'number', + default: 0, + min: 0, + max: 10, + step: 0.01, + toViewValue: v => Math.round(v * 100) + '%', + }, + seed: { + label: i18n.ts._imageEffector._fxProps.seed, + type: 'seed', + default: 100, + }, + }, +} satisfies ImageEffectorUiDefinition<typeof fn>; diff --git a/packages/frontend/src/utility/image-compositor-functions/blur.glsl b/packages/frontend/src/utility/image-compositor-functions/blur.glsl new file mode 100644 index 0000000000..e591267887 --- /dev/null +++ b/packages/frontend/src/utility/image-compositor-functions/blur.glsl @@ -0,0 +1,78 @@ +#version 300 es +precision mediump float; + +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +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 float u_radius; +uniform int u_samples; +out vec4 out_color; + +void main() { + 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; + } + + vec4 result = vec4(0.0); + float totalSamples = 0.0; + + // Make blur radius resolution-independent by using a percentage of image size + // This ensures consistent visual blur regardless of image resolution + float referenceSize = min(in_resolution.x, in_resolution.y); + float normalizedRadius = u_radius / 100.0; // Convert radius to percentage (0-15 -> 0-0.15) + vec2 blurOffset = vec2(normalizedRadius) / in_resolution * referenceSize; + + // Calculate how many samples to take in each direction + // This determines the grid density, not the blur extent + int sampleRadius = int(sqrt(float(u_samples)) / 2.0); + + // Sample in a grid pattern within the specified radius + for (int x = -sampleRadius; x <= sampleRadius; x++) { + for (int y = -sampleRadius; y <= sampleRadius; y++) { + // Normalize the grid position to [-1, 1] range + float normalizedX = float(x) / float(sampleRadius); + float normalizedY = float(y) / float(sampleRadius); + + // Scale by radius to get the actual sampling offset + vec2 offset = vec2(normalizedX, normalizedY) * blurOffset; + vec2 sampleUV = in_uv + offset; + + // Only sample if within texture bounds + if (sampleUV.x >= 0.0 && sampleUV.x <= 1.0 && sampleUV.y >= 0.0 && sampleUV.y <= 1.0) { + result += texture(in_texture, sampleUV); + totalSamples += 1.0; + } + } + } + + out_color = totalSamples > 0.0 ? result / totalSamples : texture(in_texture, in_uv); +} diff --git a/packages/frontend/src/utility/image-compositor-functions/blur.ts b/packages/frontend/src/utility/image-compositor-functions/blur.ts new file mode 100644 index 0000000000..1ab8eee6ba --- /dev/null +++ b/packages/frontend/src/utility/image-compositor-functions/blur.ts @@ -0,0 +1,93 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import shader from './blur.glsl'; +import type { ImageEffectorUiDefinition } from '../image-effector/ImageEffector.js'; +import { defineImageCompositorFunction } from '@/lib/ImageCompositor.js'; +import { i18n } from '@/i18n.js'; + +export const fn = defineImageCompositorFunction<{ + offsetX: number; + offsetY: number; + scaleX: number; + scaleY: number; + ellipse: boolean; + angle: number; + radius: number; +}>({ + shader, + 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.radius, params.radius); + gl.uniform1i(u.samples, 256); + }, +}); + +export const uiDefinition = { + name: i18n.ts._imageEffector._fxs.blur, + 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) + '°', + }, + radius: { + label: i18n.ts._imageEffector._fxProps.strength, + type: 'number', + default: 3.0, + min: 0.0, + max: 10.0, + step: 0.5, + }, + }, +} satisfies ImageEffectorUiDefinition<typeof fn>; diff --git a/packages/frontend/src/utility/image-compositor-functions/checker.glsl b/packages/frontend/src/utility/image-compositor-functions/checker.glsl new file mode 100644 index 0000000000..09d11c15d2 --- /dev/null +++ b/packages/frontend/src/utility/image-compositor-functions/checker.glsl @@ -0,0 +1,43 @@ +#version 300 es +precision mediump float; + +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +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 float u_angle; +uniform float u_scale; +uniform vec3 u_color; +uniform float u_opacity; +out vec4 out_color; + +void main() { + vec4 in_color = texture(in_texture, in_uv); + float x_ratio = max(in_resolution.x / in_resolution.y, 1.0); + float y_ratio = max(in_resolution.y / in_resolution.x, 1.0); + + float angle = -(u_angle * PI); + vec2 centeredUv = (in_uv - vec2(0.5, 0.5)) * vec2(x_ratio, y_ratio); + vec2 rotatedUV = vec2( + centeredUv.x * cos(angle) - centeredUv.y * sin(angle), + centeredUv.x * sin(angle) + centeredUv.y * cos(angle) + ); + + float fmodResult = mod(floor(u_scale * rotatedUV.x) + floor(u_scale * rotatedUV.y), 2.0); + float fin = max(sign(fmodResult), 0.0); + + out_color = vec4( + mix(in_color.r, u_color.r, fin * u_opacity), + mix(in_color.g, u_color.g, fin * u_opacity), + mix(in_color.b, u_color.b, fin * u_opacity), + in_color.a + ); +} diff --git a/packages/frontend/src/utility/image-compositor-functions/checker.ts b/packages/frontend/src/utility/image-compositor-functions/checker.ts new file mode 100644 index 0000000000..e0476bb126 --- /dev/null +++ b/packages/frontend/src/utility/image-compositor-functions/checker.ts @@ -0,0 +1,61 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import shader from './checker.glsl'; +import type { ImageEffectorUiDefinition } from '../image-effector/ImageEffector.js'; +import { defineImageCompositorFunction } from '@/lib/ImageCompositor.js'; +import { i18n } from '@/i18n.js'; + +export const fn = defineImageCompositorFunction<{ + angle: number; + scale: number; + color: [number, number, number]; + opacity: number; +}>({ + shader, + main: ({ gl, u, params }) => { + gl.uniform1f(u.angle, params.angle / 2); + gl.uniform1f(u.scale, params.scale * params.scale); + gl.uniform3f(u.color, params.color[0], params.color[1], params.color[2]); + gl.uniform1f(u.opacity, params.opacity); + }, +}); + +export const uiDefinition = { + name: i18n.ts._imageEffector._fxs.checker, + params: { + 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) + '°', + }, + scale: { + label: i18n.ts._imageEffector._fxProps.scale, + type: 'number', + default: 3.0, + min: 1.0, + max: 10.0, + step: 0.1, + }, + color: { + label: i18n.ts._imageEffector._fxProps.color, + type: 'color', + default: [1, 1, 1], + }, + opacity: { + label: i18n.ts._imageEffector._fxProps.opacity, + type: 'number', + default: 0.5, + min: 0.0, + max: 1.0, + step: 0.01, + toViewValue: v => Math.round(v * 100) + '%', + }, + }, +} satisfies ImageEffectorUiDefinition<typeof fn>; diff --git a/packages/frontend/src/utility/image-compositor-functions/chromaticAberration.glsl b/packages/frontend/src/utility/image-compositor-functions/chromaticAberration.glsl new file mode 100644 index 0000000000..60bb4f5318 --- /dev/null +++ b/packages/frontend/src/utility/image-compositor-functions/chromaticAberration.glsl @@ -0,0 +1,49 @@ +#version 300 es +precision mediump float; + +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +in vec2 in_uv; +uniform sampler2D in_texture; +uniform vec2 in_resolution; +out vec4 out_color; +uniform float u_amount; +uniform float u_start; +uniform bool u_normalize; + +void main() { + int samples = 64; + float r_strength = 1.0; + float g_strength = 1.5; + float b_strength = 2.0; + + vec2 size = vec2(in_resolution.x, in_resolution.y); + + vec4 accumulator = vec4(0.0); + float normalisedValue = length((in_uv - 0.5) * 2.0); + float strength = clamp((normalisedValue - u_start) * (1.0 / (1.0 - u_start)), 0.0, 1.0); + + vec2 vector = (u_normalize ? normalize(in_uv - vec2(0.5)) : in_uv - vec2(0.5)); + vec2 velocity = vector * strength * u_amount; + + vec2 rOffset = -vector * strength * (u_amount * r_strength); + vec2 gOffset = -vector * strength * (u_amount * g_strength); + vec2 bOffset = -vector * strength * (u_amount * b_strength); + + for (int i = 0; i < samples; i++) { + accumulator.r += texture(in_texture, in_uv + rOffset).r; + rOffset -= velocity / float(samples); + + accumulator.g += texture(in_texture, in_uv + gOffset).g; + gOffset -= velocity / float(samples); + + accumulator.b += texture(in_texture, in_uv + bOffset).b; + bOffset -= velocity / float(samples); + } + + out_color = vec4(vec3(accumulator / float(samples)), 1.0); +} + diff --git a/packages/frontend/src/utility/image-compositor-functions/chromaticAberration.ts b/packages/frontend/src/utility/image-compositor-functions/chromaticAberration.ts new file mode 100644 index 0000000000..5e327dd6ac --- /dev/null +++ b/packages/frontend/src/utility/image-compositor-functions/chromaticAberration.ts @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import shader from './chromaticAberration.glsl'; +import type { ImageEffectorUiDefinition } from '../image-effector/ImageEffector.js'; +import { defineImageCompositorFunction } from '@/lib/ImageCompositor.js'; +import { i18n } from '@/i18n.js'; + +export const fn = defineImageCompositorFunction<{ + normalize: boolean; + amount: number; +}>({ + shader, + main: ({ gl, u, params }) => { + gl.uniform1f(u.amount, params.amount); + gl.uniform1i(u.normalize, params.normalize ? 1 : 0); + }, +}); + +export const uiDefinition = { + name: i18n.ts._imageEffector._fxs.chromaticAberration, + params: { + normalize: { + label: i18n.ts._imageEffector._fxProps.normalize, + type: 'boolean', + default: false, + }, + amount: { + label: i18n.ts._imageEffector._fxProps.amount, + type: 'number', + default: 0.1, + min: 0.0, + max: 1.0, + step: 0.01, + }, + }, +} satisfies ImageEffectorUiDefinition<typeof fn>; diff --git a/packages/frontend/src/utility/image-compositor-functions/colorAdjust.glsl b/packages/frontend/src/utility/image-compositor-functions/colorAdjust.glsl new file mode 100644 index 0000000000..2d0c87ce95 --- /dev/null +++ b/packages/frontend/src/utility/image-compositor-functions/colorAdjust.glsl @@ -0,0 +1,82 @@ +#version 300 es +precision mediump float; + +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +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(u_lightness); + 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); +} diff --git a/packages/frontend/src/utility/image-compositor-functions/colorAdjust.ts b/packages/frontend/src/utility/image-compositor-functions/colorAdjust.ts new file mode 100644 index 0000000000..33ca05ace7 --- /dev/null +++ b/packages/frontend/src/utility/image-compositor-functions/colorAdjust.ts @@ -0,0 +1,77 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import shader from './colorAdjust.glsl'; +import type { ImageEffectorUiDefinition } from '../image-effector/ImageEffector.js'; +import { defineImageCompositorFunction } from '@/lib/ImageCompositor.js'; +import { i18n } from '@/i18n.js'; + +export const fn = defineImageCompositorFunction<{ + lightness: number; + contrast: number; + hue: number; + brightness: number; + saturation: number; +}>({ + shader, + main: ({ gl, u, params }) => { + gl.uniform1f(u.brightness, params.brightness); + gl.uniform1f(u.contrast, params.contrast); + gl.uniform1f(u.hue, params.hue / 2); + gl.uniform1f(u.lightness, params.lightness); + gl.uniform1f(u.saturation, params.saturation); + }, +}); + +export const uiDefinition = { + name: i18n.ts._imageEffector._fxs.colorAdjust, + params: { + lightness: { + label: i18n.ts._imageEffector._fxProps.lightness, + type: 'number', + default: 0, + min: -1, + max: 1, + step: 0.01, + toViewValue: v => Math.round(v * 100) + '%', + }, + contrast: { + label: i18n.ts._imageEffector._fxProps.contrast, + type: 'number', + default: 1, + min: 0, + max: 4, + step: 0.01, + toViewValue: v => Math.round(v * 100) + '%', + }, + hue: { + label: i18n.ts._imageEffector._fxProps.hue, + type: 'number', + default: 0, + min: -1, + max: 1, + step: 0.01, + toViewValue: v => Math.round(v * 180) + '°', + }, + brightness: { + label: i18n.ts._imageEffector._fxProps.brightness, + type: 'number', + default: 1, + min: 0, + max: 4, + step: 0.01, + toViewValue: v => Math.round(v * 100) + '%', + }, + saturation: { + label: i18n.ts._imageEffector._fxProps.saturation, + type: 'number', + default: 1, + min: 0, + max: 4, + step: 0.01, + toViewValue: v => Math.round(v * 100) + '%', + }, + }, +} satisfies ImageEffectorUiDefinition<typeof fn>; diff --git a/packages/frontend/src/utility/image-compositor-functions/colorClamp.glsl b/packages/frontend/src/utility/image-compositor-functions/colorClamp.glsl new file mode 100644 index 0000000000..bf37f5ab43 --- /dev/null +++ b/packages/frontend/src/utility/image-compositor-functions/colorClamp.glsl @@ -0,0 +1,29 @@ +#version 300 es +precision mediump float; + +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +// colorClamp, colorClampAdvanced共通 +// colorClampではmax, minがすべて同じ値となる + +in vec2 in_uv; +uniform sampler2D in_texture; +uniform vec2 in_resolution; +uniform float u_rMax; +uniform float u_rMin; +uniform float u_gMax; +uniform float u_gMin; +uniform float u_bMax; +uniform float u_bMin; +out vec4 out_color; + +void main() { + vec4 in_color = texture(in_texture, in_uv); + float r = min(max(in_color.r, u_rMin), u_rMax); + float g = min(max(in_color.g, u_gMin), u_gMax); + float b = min(max(in_color.b, u_bMin), u_bMax); + out_color = vec4(r, g, b, in_color.a); +} diff --git a/packages/frontend/src/utility/image-compositor-functions/colorClamp.ts b/packages/frontend/src/utility/image-compositor-functions/colorClamp.ts new file mode 100644 index 0000000000..d4e7b786d0 --- /dev/null +++ b/packages/frontend/src/utility/image-compositor-functions/colorClamp.ts @@ -0,0 +1,48 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import shader from './colorClamp.glsl'; +import type { ImageEffectorUiDefinition } from '../image-effector/ImageEffector.js'; +import { defineImageCompositorFunction } from '@/lib/ImageCompositor.js'; +import { i18n } from '@/i18n.js'; + +export const fn = defineImageCompositorFunction<{ + max: number; + min: number; +}>({ + shader, + main: ({ gl, u, params }) => { + gl.uniform1f(u.rMax, params.max); + gl.uniform1f(u.rMin, 1.0 + params.min); + gl.uniform1f(u.gMax, params.max); + gl.uniform1f(u.gMin, 1.0 + params.min); + gl.uniform1f(u.bMax, params.max); + gl.uniform1f(u.bMin, 1.0 + params.min); + }, +}); + +export const uiDefinition = { + name: i18n.ts._imageEffector._fxs.colorClamp, + params: { + max: { + label: i18n.ts._imageEffector._fxProps.max, + type: 'number', + default: 1.0, + min: 0.0, + max: 1.0, + step: 0.01, + toViewValue: v => Math.round(v * 100) + '%', + }, + min: { + label: i18n.ts._imageEffector._fxProps.min, + type: 'number', + default: -1.0, + min: -1.0, + max: 0.0, + step: 0.01, + toViewValue: v => Math.round(v * 100) + '%', + }, + }, +} satisfies ImageEffectorUiDefinition<typeof fn>; diff --git a/packages/frontend/src/utility/image-compositor-functions/colorClampAdvanced.ts b/packages/frontend/src/utility/image-compositor-functions/colorClampAdvanced.ts new file mode 100644 index 0000000000..492524ec06 --- /dev/null +++ b/packages/frontend/src/utility/image-compositor-functions/colorClampAdvanced.ts @@ -0,0 +1,88 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import shader from './colorClamp.glsl'; +import type { ImageEffectorUiDefinition } from '../image-effector/ImageEffector.js'; +import { defineImageCompositorFunction } from '@/lib/ImageCompositor.js'; +import { i18n } from '@/i18n.js'; + +export const fn = defineImageCompositorFunction<{ + rMax: number; + rMin: number; + gMax: number; + gMin: number; + bMax: number; + bMin: number; +}>({ + shader, + main: ({ gl, u, params }) => { + gl.uniform1f(u.rMax, params.rMax); + gl.uniform1f(u.rMin, 1.0 + params.rMin); + gl.uniform1f(u.gMax, params.gMax); + gl.uniform1f(u.gMin, 1.0 + params.gMin); + gl.uniform1f(u.bMax, params.bMax); + gl.uniform1f(u.bMin, 1.0 + params.bMin); + }, +}); + +export const uiDefinition = { + name: i18n.ts._imageEffector._fxs.colorClampAdvanced, + params: { + rMax: { + label: `${i18n.ts._imageEffector._fxProps.max} (${i18n.ts._imageEffector._fxProps.redComponent})`, + type: 'number', + default: 1.0, + min: 0.0, + max: 1.0, + step: 0.01, + toViewValue: v => Math.round(v * 100) + '%', + }, + rMin: { + label: `${i18n.ts._imageEffector._fxProps.min} (${i18n.ts._imageEffector._fxProps.redComponent})`, + type: 'number', + default: -1.0, + min: -1.0, + max: 0.0, + step: 0.01, + toViewValue: v => Math.round(v * 100) + '%', + }, + gMax: { + label: `${i18n.ts._imageEffector._fxProps.max} (${i18n.ts._imageEffector._fxProps.greenComponent})`, + type: 'number', + default: 1.0, + min: 0.0, + max: 1.0, + step: 0.01, + toViewValue: v => Math.round(v * 100) + '%', + }, + gMin: { + label: `${i18n.ts._imageEffector._fxProps.min} (${i18n.ts._imageEffector._fxProps.greenComponent})`, + type: 'number', + default: -1.0, + min: -1.0, + max: 0.0, + step: 0.01, + toViewValue: v => Math.round(v * 100) + '%', + }, + bMax: { + label: `${i18n.ts._imageEffector._fxProps.max} (${i18n.ts._imageEffector._fxProps.blueComponent})`, + type: 'number', + default: 1.0, + min: 0.0, + max: 1.0, + step: 0.01, + toViewValue: v => Math.round(v * 100) + '%', + }, + bMin: { + label: `${i18n.ts._imageEffector._fxProps.min} (${i18n.ts._imageEffector._fxProps.blueComponent})`, + type: 'number', + default: -1.0, + min: -1.0, + max: 0.0, + step: 0.01, + toViewValue: v => Math.round(v * 100) + '%', + }, + }, +} satisfies ImageEffectorUiDefinition<typeof fn>; diff --git a/packages/frontend/src/utility/image-compositor-functions/distort.glsl b/packages/frontend/src/utility/image-compositor-functions/distort.glsl new file mode 100644 index 0000000000..7e0d1e3252 --- /dev/null +++ b/packages/frontend/src/utility/image-compositor-functions/distort.glsl @@ -0,0 +1,30 @@ +#version 300 es +precision mediump float; + +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +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 float u_phase; +uniform float u_frequency; +uniform float u_strength; +uniform int u_direction; // 0: vertical, 1: horizontal +out vec4 out_color; + +void main() { + float v = u_direction == 0 ? + sin((HALF_PI + (u_phase * PI) - (u_frequency / 2.0)) + in_uv.y * u_frequency) * u_strength : + sin((HALF_PI + (u_phase * PI) - (u_frequency / 2.0)) + in_uv.x * u_frequency) * u_strength; + vec4 in_color = u_direction == 0 ? + texture(in_texture, vec2(in_uv.x + v, in_uv.y)) : + texture(in_texture, vec2(in_uv.x, in_uv.y + v)); + out_color = in_color; +} diff --git a/packages/frontend/src/utility/image-compositor-functions/distort.ts b/packages/frontend/src/utility/image-compositor-functions/distort.ts new file mode 100644 index 0000000000..bd0fcdf42f --- /dev/null +++ b/packages/frontend/src/utility/image-compositor-functions/distort.ts @@ -0,0 +1,65 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import shader from './distort.glsl'; +import type { ImageEffectorUiDefinition } from '../image-effector/ImageEffector.js'; +import { defineImageCompositorFunction } from '@/lib/ImageCompositor.js'; +import { i18n } from '@/i18n.js'; + +export const fn = defineImageCompositorFunction<{ + direction: number; + phase: number; + frequency: number; + strength: number; +}>({ + shader, + main: ({ gl, u, params }) => { + gl.uniform1f(u.phase, params.phase); + gl.uniform1f(u.frequency, params.frequency); + gl.uniform1f(u.strength, params.strength); + gl.uniform1i(u.direction, params.direction); + }, +}); + +export const uiDefinition = { + name: i18n.ts._imageEffector._fxs.distort, + params: { + direction: { + label: i18n.ts._imageEffector._fxProps.direction, + type: 'number:enum', + enum: [ + { value: 0 as const, label: i18n.ts.horizontal }, + { value: 1 as const, label: i18n.ts.vertical }, + ], + default: 1, + }, + phase: { + label: i18n.ts._imageEffector._fxProps.phase, + type: 'number', + default: 0.0, + min: -1.0, + max: 1.0, + step: 0.01, + toViewValue: v => Math.round(v * 100) + '%', + }, + frequency: { + label: i18n.ts._imageEffector._fxProps.frequency, + type: 'number', + default: 30, + min: 0, + max: 100, + step: 0.1, + }, + strength: { + label: i18n.ts._imageEffector._fxProps.strength, + type: 'number', + default: 0.05, + min: 0, + max: 1, + step: 0.01, + toViewValue: v => Math.round(v * 100) + '%', + }, + }, +} satisfies ImageEffectorUiDefinition<typeof fn>; diff --git a/packages/frontend/src/utility/image-compositor-functions/fill.glsl b/packages/frontend/src/utility/image-compositor-functions/fill.glsl new file mode 100644 index 0000000000..f04dc5545a --- /dev/null +++ b/packages/frontend/src/utility/image-compositor-functions/fill.glsl @@ -0,0 +1,50 @@ +#version 300 es +precision mediump float; + +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +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 vec3 u_color; +uniform float u_opacity; +out vec4 out_color; + +void main() { + vec4 in_color = texture(in_texture, in_uv); + //float x_ratio = max(in_resolution.x / in_resolution.y, 1.0); + //float y_ratio = max(in_resolution.y / in_resolution.x, 1.0); + + 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; + } + + out_color = isInside ? vec4( + mix(in_color.r, u_color.r, u_opacity), + mix(in_color.g, u_color.g, u_opacity), + mix(in_color.b, u_color.b, u_opacity), + in_color.a + ) : in_color; +} diff --git a/packages/frontend/src/utility/image-compositor-functions/fill.ts b/packages/frontend/src/utility/image-compositor-functions/fill.ts new file mode 100644 index 0000000000..901bdadfe5 --- /dev/null +++ b/packages/frontend/src/utility/image-compositor-functions/fill.ts @@ -0,0 +1,100 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import shader from './fill.glsl'; +import type { ImageEffectorUiDefinition } from '../image-effector/ImageEffector.js'; +import { defineImageCompositorFunction } from '@/lib/ImageCompositor.js'; +import { i18n } from '@/i18n.js'; + +export const fn = defineImageCompositorFunction<{ + offsetX: number; + offsetY: number; + scaleX: number; + scaleY: number; + ellipse: boolean; + angle: number; + color: [number, number, number]; + opacity: number; +}>({ + shader, + 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.uniform3f(u.color, params.color[0], params.color[1], params.color[2]); + gl.uniform1f(u.opacity, params.opacity); + }, +}); + +export const uiDefinition = { + name: i18n.ts._imageEffector._fxs.fill, + 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) + '°', + }, + color: { + label: i18n.ts._imageEffector._fxProps.color, + type: 'color', + default: [1, 1, 1], + }, + opacity: { + label: i18n.ts._imageEffector._fxProps.opacity, + type: 'number', + default: 1.0, + min: 0.0, + max: 1.0, + step: 0.01, + toViewValue: v => Math.round(v * 100) + '%', + }, + }, +} satisfies ImageEffectorUiDefinition<typeof fn>; diff --git a/packages/frontend/src/utility/image-compositor-functions/grayscale.glsl b/packages/frontend/src/utility/image-compositor-functions/grayscale.glsl new file mode 100644 index 0000000000..54ca719976 --- /dev/null +++ b/packages/frontend/src/utility/image-compositor-functions/grayscale.glsl @@ -0,0 +1,22 @@ +#version 300 es +precision mediump float; + +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +in vec2 in_uv; +uniform sampler2D in_texture; +uniform vec2 in_resolution; +out vec4 out_color; + +float getBrightness(vec4 color) { + return (color.r + color.g + color.b) / 3.0; +} + +void main() { + vec4 in_color = texture(in_texture, in_uv); + float brightness = getBrightness(in_color); + out_color = vec4(brightness, brightness, brightness, in_color.a); +} diff --git a/packages/frontend/src/utility/image-compositor-functions/grayscale.ts b/packages/frontend/src/utility/image-compositor-functions/grayscale.ts new file mode 100644 index 0000000000..b6860de0a2 --- /dev/null +++ b/packages/frontend/src/utility/image-compositor-functions/grayscale.ts @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import shader from './grayscale.glsl'; +import type { ImageEffectorUiDefinition } from '../image-effector/ImageEffector.js'; +import { defineImageCompositorFunction } from '@/lib/ImageCompositor.js'; +import { i18n } from '@/i18n.js'; + +export const fn = defineImageCompositorFunction({ + shader, + main: ({ gl, u, params }) => { + }, +}); + +export const uiDefinition = { + name: i18n.ts._imageEffector._fxs.grayscale, + params: { + }, +} satisfies ImageEffectorUiDefinition<typeof fn>; diff --git a/packages/frontend/src/utility/image-compositor-functions/invert.glsl b/packages/frontend/src/utility/image-compositor-functions/invert.glsl new file mode 100644 index 0000000000..a2d1574f5b --- /dev/null +++ b/packages/frontend/src/utility/image-compositor-functions/invert.glsl @@ -0,0 +1,23 @@ +#version 300 es +precision mediump float; + +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +in vec2 in_uv; +uniform sampler2D in_texture; +uniform vec2 in_resolution; +uniform bool u_r; +uniform bool u_g; +uniform bool u_b; +out vec4 out_color; + +void main() { + vec4 in_color = texture(in_texture, in_uv); + out_color.r = u_r ? 1.0 - in_color.r : in_color.r; + out_color.g = u_g ? 1.0 - in_color.g : in_color.g; + out_color.b = u_b ? 1.0 - in_color.b : in_color.b; + out_color.a = in_color.a; +} diff --git a/packages/frontend/src/utility/image-compositor-functions/invert.ts b/packages/frontend/src/utility/image-compositor-functions/invert.ts new file mode 100644 index 0000000000..f64e68034e --- /dev/null +++ b/packages/frontend/src/utility/image-compositor-functions/invert.ts @@ -0,0 +1,43 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import shader from './invert.glsl'; +import type { ImageEffectorUiDefinition } from '../image-effector/ImageEffector.js'; +import { defineImageCompositorFunction } from '@/lib/ImageCompositor.js'; +import { i18n } from '@/i18n.js'; + +export const fn = defineImageCompositorFunction<{ + r: boolean; + g: boolean; + b: boolean; +}>({ + shader, + main: ({ gl, u, params }) => { + gl.uniform1i(u.r, params.r ? 1 : 0); + gl.uniform1i(u.g, params.g ? 1 : 0); + gl.uniform1i(u.b, params.b ? 1 : 0); + }, +}); + +export const uiDefinition = { + name: i18n.ts._imageEffector._fxs.invert, + params: { + r: { + label: i18n.ts._imageEffector._fxProps.redComponent, + type: 'boolean', + default: true, + }, + g: { + label: i18n.ts._imageEffector._fxProps.greenComponent, + type: 'boolean', + default: true, + }, + b: { + label: i18n.ts._imageEffector._fxProps.blueComponent, + type: 'boolean', + default: true, + }, + }, +} satisfies ImageEffectorUiDefinition<typeof fn>; diff --git a/packages/frontend/src/utility/image-compositor-functions/mirror.glsl b/packages/frontend/src/utility/image-compositor-functions/mirror.glsl new file mode 100644 index 0000000000..b27934e9ef --- /dev/null +++ b/packages/frontend/src/utility/image-compositor-functions/mirror.glsl @@ -0,0 +1,26 @@ +#version 300 es +precision mediump float; + +in vec2 in_uv; +uniform sampler2D in_texture; +uniform vec2 in_resolution; +uniform int u_h; +uniform int u_v; +out vec4 out_color; + +void main() { + vec2 uv = in_uv; + if (u_h == -1 && in_uv.x > 0.5) { + uv.x = 1.0 - uv.x; + } + if (u_h == 1 && in_uv.x < 0.5) { + uv.x = 1.0 - uv.x; + } + if (u_v == -1 && in_uv.y > 0.5) { + uv.y = 1.0 - uv.y; + } + if (u_v == 1 && in_uv.y < 0.5) { + uv.y = 1.0 - uv.y; + } + out_color = texture(in_texture, uv); +} diff --git a/packages/frontend/src/utility/image-compositor-functions/mirror.ts b/packages/frontend/src/utility/image-compositor-functions/mirror.ts new file mode 100644 index 0000000000..47d19c0553 --- /dev/null +++ b/packages/frontend/src/utility/image-compositor-functions/mirror.ts @@ -0,0 +1,46 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import shader from './mirror.glsl'; +import type { ImageEffectorUiDefinition } from '../image-effector/ImageEffector.js'; +import { defineImageCompositorFunction } from '@/lib/ImageCompositor.js'; +import { i18n } from '@/i18n.js'; + +export const fn = defineImageCompositorFunction<{ + h: number; + v: number; +}>({ + shader, + main: ({ gl, u, params }) => { + gl.uniform1i(u.h, params.h); + gl.uniform1i(u.v, params.v); + }, +}); + +export const uiDefinition = { + name: i18n.ts._imageEffector._fxs.mirror, + params: { + h: { + label: i18n.ts.horizontal, + type: 'number:enum', + enum: [ + { value: -1 as const, icon: 'ti ti-arrow-bar-right' }, + { value: 0 as const, icon: 'ti ti-minus-vertical' }, + { value: 1 as const, icon: 'ti ti-arrow-bar-left' }, + ], + default: -1, + }, + v: { + label: i18n.ts.vertical, + type: 'number:enum', + enum: [ + { value: -1 as const, icon: 'ti ti-arrow-bar-down' }, + { value: 0 as const, icon: 'ti ti-minus' }, + { value: 1 as const, icon: 'ti ti-arrow-bar-up' }, + ], + default: 0, + }, + }, +} satisfies ImageEffectorUiDefinition<typeof fn>; diff --git a/packages/frontend/src/utility/image-compositor-functions/pixelate.glsl b/packages/frontend/src/utility/image-compositor-functions/pixelate.glsl new file mode 100644 index 0000000000..4de3f27397 --- /dev/null +++ b/packages/frontend/src/utility/image-compositor-functions/pixelate.glsl @@ -0,0 +1,68 @@ +#version 300 es +precision mediump float; + +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +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); +} diff --git a/packages/frontend/src/utility/image-compositor-functions/pixelate.ts b/packages/frontend/src/utility/image-compositor-functions/pixelate.ts new file mode 100644 index 0000000000..249d272e7e --- /dev/null +++ b/packages/frontend/src/utility/image-compositor-functions/pixelate.ts @@ -0,0 +1,93 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import shader from './pixelate.glsl'; +import type { ImageEffectorUiDefinition } from '../image-effector/ImageEffector.js'; +import { defineImageCompositorFunction } from '@/lib/ImageCompositor.js'; +import { i18n } from '@/i18n.js'; + +export const fn = defineImageCompositorFunction<{ + offsetX: number; + offsetY: number; + scaleX: number; + scaleY: number; + ellipse: boolean; + angle: number; + strength: number; +}>({ + shader, + 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); + }, +}); + +export const uiDefinition = { + name: i18n.ts._imageEffector._fxs.pixelate, + 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, + }, + }, +} satisfies ImageEffectorUiDefinition<typeof fn>; diff --git a/packages/frontend/src/utility/image-compositor-functions/polkadot.glsl b/packages/frontend/src/utility/image-compositor-functions/polkadot.glsl new file mode 100644 index 0000000000..39ecad34b5 --- /dev/null +++ b/packages/frontend/src/utility/image-compositor-functions/polkadot.glsl @@ -0,0 +1,75 @@ +#version 300 es +precision mediump float; + +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +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 float u_angle; +uniform float u_scale; +uniform float u_major_radius; +uniform float u_major_opacity; +uniform float u_minor_divisions; +uniform float u_minor_radius; +uniform float u_minor_opacity; +uniform vec3 u_color; +out vec4 out_color; + +void main() { + vec4 in_color = texture(in_texture, in_uv); + float x_ratio = max(in_resolution.x / in_resolution.y, 1.0); + float y_ratio = max(in_resolution.y / in_resolution.x, 1.0); + + float angle = -(u_angle * PI); + vec2 centeredUv = (in_uv - vec2(0.5, 0.5)) * vec2(x_ratio, y_ratio); + vec2 rotatedUV = vec2( + centeredUv.x * cos(angle) - centeredUv.y * sin(angle), + centeredUv.x * sin(angle) + centeredUv.y * cos(angle) + ); + + float major_modX = mod(rotatedUV.x, (1.0 / u_scale)); + float major_modY = mod(rotatedUV.y, (1.0 / u_scale)); + float major_threshold = ((u_major_radius / 2.0) / u_scale); + if ( + length(vec2(major_modX, major_modY)) < major_threshold || + length(vec2((1.0 / u_scale) - major_modX, major_modY)) < major_threshold || + length(vec2(major_modX, (1.0 / u_scale) - major_modY)) < major_threshold || + length(vec2((1.0 / u_scale) - major_modX, (1.0 / u_scale) - major_modY)) < major_threshold + ) { + out_color = vec4( + mix(in_color.r, u_color.r, u_major_opacity), + mix(in_color.g, u_color.g, u_major_opacity), + mix(in_color.b, u_color.b, u_major_opacity), + in_color.a + ); + return; + } + + float minor_modX = mod(rotatedUV.x, (1.0 / u_scale / u_minor_divisions)); + float minor_modY = mod(rotatedUV.y, (1.0 / u_scale / u_minor_divisions)); + float minor_threshold = ((u_minor_radius / 2.0) / (u_minor_divisions * u_scale)); + if ( + length(vec2(minor_modX, minor_modY)) < minor_threshold || + length(vec2((1.0 / u_scale / u_minor_divisions) - minor_modX, minor_modY)) < minor_threshold || + length(vec2(minor_modX, (1.0 / u_scale / u_minor_divisions) - minor_modY)) < minor_threshold || + length(vec2((1.0 / u_scale / u_minor_divisions) - minor_modX, (1.0 / u_scale / u_minor_divisions) - minor_modY)) < minor_threshold + ) { + out_color = vec4( + mix(in_color.r, u_color.r, u_minor_opacity), + mix(in_color.g, u_color.g, u_minor_opacity), + mix(in_color.b, u_color.b, u_minor_opacity), + in_color.a + ); + return; + } + + out_color = in_color; +} diff --git a/packages/frontend/src/utility/image-compositor-functions/polkadot.ts b/packages/frontend/src/utility/image-compositor-functions/polkadot.ts new file mode 100644 index 0000000000..d94d704be3 --- /dev/null +++ b/packages/frontend/src/utility/image-compositor-functions/polkadot.ts @@ -0,0 +1,102 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import shader from './polkadot.glsl'; +import type { ImageEffectorUiDefinition } from '../image-effector/ImageEffector.js'; +import { defineImageCompositorFunction } from '@/lib/ImageCompositor.js'; +import { i18n } from '@/i18n.js'; + +export const fn = defineImageCompositorFunction<{ + angle: number; + scale: number; + majorRadius: number; + majorOpacity: number; + minorDivisions: number; + minorRadius: number; + minorOpacity: number; + color: [number, number, number]; +}>({ + shader, + main: ({ gl, u, params }) => { + gl.uniform1f(u.angle, params.angle / 2); + gl.uniform1f(u.scale, params.scale * params.scale); + gl.uniform1f(u.major_radius, params.majorRadius); + gl.uniform1f(u.major_opacity, params.majorOpacity); + gl.uniform1f(u.minor_divisions, params.minorDivisions); + gl.uniform1f(u.minor_radius, params.minorRadius); + gl.uniform3f(u.color, params.color[0], params.color[1], params.color[2]); + gl.uniform1f(u.minor_opacity, params.minorOpacity); + }, +}); + +export const uiDefinition = { + name: i18n.ts._imageEffector._fxs.polkadot, + params: { + 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) + '°', + }, + scale: { + label: i18n.ts._imageEffector._fxProps.scale, + type: 'number', + default: 3.0, + min: 1.0, + max: 10.0, + step: 0.1, + }, + majorRadius: { + label: i18n.ts._watermarkEditor.polkadotMainDotRadius, + type: 'number', + default: 0.1, + min: 0.0, + max: 1.0, + step: 0.01, + }, + majorOpacity: { + label: i18n.ts._watermarkEditor.polkadotMainDotOpacity, + type: 'number', + default: 0.75, + min: 0.0, + max: 1.0, + step: 0.01, + toViewValue: v => Math.round(v * 100) + '%', + }, + minorDivisions: { + label: i18n.ts._watermarkEditor.polkadotSubDotDivisions, + type: 'number', + default: 4, + min: 0, + max: 16, + step: 1, + }, + minorRadius: { + label: i18n.ts._watermarkEditor.polkadotSubDotRadius, + type: 'number', + default: 0.25, + min: 0.0, + max: 1.0, + step: 0.01, + }, + minorOpacity: { + label: i18n.ts._watermarkEditor.polkadotSubDotOpacity, + type: 'number', + default: 0.5, + min: 0.0, + max: 1.0, + step: 0.01, + toViewValue: v => Math.round(v * 100) + '%', + }, + color: { + label: i18n.ts._imageEffector._fxProps.color, + type: 'color', + default: [1, 1, 1], + }, + }, +} satisfies ImageEffectorUiDefinition<typeof fn>; diff --git a/packages/frontend/src/utility/image-compositor-functions/stripe.glsl b/packages/frontend/src/utility/image-compositor-functions/stripe.glsl new file mode 100644 index 0000000000..bb18d8fcb8 --- /dev/null +++ b/packages/frontend/src/utility/image-compositor-functions/stripe.glsl @@ -0,0 +1,45 @@ +#version 300 es +precision mediump float; + +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +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 float u_angle; +uniform float u_frequency; +uniform float u_phase; +uniform float u_threshold; +uniform vec3 u_color; +uniform float u_opacity; +out vec4 out_color; + +void main() { + vec4 in_color = texture(in_texture, in_uv); + float x_ratio = max(in_resolution.x / in_resolution.y, 1.0); + float y_ratio = max(in_resolution.y / in_resolution.x, 1.0); + + float angle = -(u_angle * PI); + vec2 centeredUv = (in_uv - vec2(0.5, 0.5)) * vec2(x_ratio, y_ratio); + vec2 rotatedUV = vec2( + centeredUv.x * cos(angle) - centeredUv.y * sin(angle), + centeredUv.x * sin(angle) + centeredUv.y * cos(angle) + ); + + float phase = u_phase * TWO_PI; + float value = (1.0 + sin((rotatedUV.x * u_frequency - HALF_PI) + phase)) / 2.0; + value = value < u_threshold ? 1.0 : 0.0; + out_color = vec4( + mix(in_color.r, u_color.r, value * u_opacity), + mix(in_color.g, u_color.g, value * u_opacity), + mix(in_color.b, u_color.b, value * u_opacity), + in_color.a + ); +} diff --git a/packages/frontend/src/utility/image-compositor-functions/stripe.ts b/packages/frontend/src/utility/image-compositor-functions/stripe.ts new file mode 100644 index 0000000000..d429a124bc --- /dev/null +++ b/packages/frontend/src/utility/image-compositor-functions/stripe.ts @@ -0,0 +1,73 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import shader from './stripe.glsl'; +import type { ImageEffectorUiDefinition } from '../image-effector/ImageEffector.js'; +import { defineImageCompositorFunction } from '@/lib/ImageCompositor.js'; +import { i18n } from '@/i18n.js'; + +export const fn = defineImageCompositorFunction<{ + angle: number; + frequency: number; + threshold: number; + color: [number, number, number]; + opacity: number; +}>({ + shader, + main: ({ gl, u, params }) => { + gl.uniform1f(u.angle, params.angle / 2); + gl.uniform1f(u.frequency, params.frequency * params.frequency); + gl.uniform1f(u.phase, 0.0); + gl.uniform1f(u.threshold, params.threshold); + gl.uniform3f(u.color, params.color[0], params.color[1], params.color[2]); + gl.uniform1f(u.opacity, params.opacity); + }, +}); + +export const uiDefinition = { + name: i18n.ts._imageEffector._fxs.stripe, + params: { + angle: { + label: i18n.ts._imageEffector._fxProps.angle, + type: 'number', + default: 0.5, + min: -1.0, + max: 1.0, + step: 0.01, + toViewValue: v => Math.round(v * 90) + '°', + }, + frequency: { + label: i18n.ts._watermarkEditor.stripeFrequency, + type: 'number', + default: 10.0, + min: 1.0, + max: 30.0, + step: 0.1, + }, + threshold: { + label: i18n.ts._watermarkEditor.stripeWidth, + type: 'number', + default: 0.1, + min: 0.0, + max: 1.0, + step: 0.01, + toViewValue: v => Math.round(v * 100) + '%', + }, + color: { + label: i18n.ts._imageEffector._fxProps.color, + type: 'color', + default: [1, 1, 1], + }, + opacity: { + label: i18n.ts._imageEffector._fxProps.opacity, + type: 'number', + default: 0.5, + min: 0.0, + max: 1.0, + step: 0.01, + toViewValue: v => Math.round(v * 100) + '%', + }, + }, +} satisfies ImageEffectorUiDefinition<typeof fn>; diff --git a/packages/frontend/src/utility/image-compositor-functions/tearing.glsl b/packages/frontend/src/utility/image-compositor-functions/tearing.glsl new file mode 100644 index 0000000000..3fb2fc2cad --- /dev/null +++ b/packages/frontend/src/utility/image-compositor-functions/tearing.glsl @@ -0,0 +1,33 @@ +#version 300 es +precision mediump float; + +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +in vec2 in_uv; +uniform sampler2D in_texture; +uniform vec2 in_resolution; +uniform int u_amount; +uniform float u_shiftStrengths[128]; +uniform float u_shiftOrigins[128]; +uniform float u_shiftHeights[128]; +uniform float u_channelShift; +out vec4 out_color; + +void main() { + float v = 0.0; + + for (int i = 0; i < u_amount; i++) { + if (in_uv.y > (u_shiftOrigins[i] - u_shiftHeights[i]) && in_uv.y < (u_shiftOrigins[i] + u_shiftHeights[i])) { + v += u_shiftStrengths[i]; + } + } + + float r = texture(in_texture, vec2(in_uv.x + (v * (1.0 + u_channelShift)), in_uv.y)).r; + float g = texture(in_texture, vec2(in_uv.x + v, in_uv.y)).g; + float b = texture(in_texture, vec2(in_uv.x + (v * (1.0 + (u_channelShift / 2.0))), in_uv.y)).b; + float a = texture(in_texture, vec2(in_uv.x + v, in_uv.y)).a; + out_color = vec4(r, g, b, a); +} diff --git a/packages/frontend/src/utility/image-compositor-functions/tearing.ts b/packages/frontend/src/utility/image-compositor-functions/tearing.ts new file mode 100644 index 0000000000..66c61b7ca8 --- /dev/null +++ b/packages/frontend/src/utility/image-compositor-functions/tearing.ts @@ -0,0 +1,83 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import seedrandom from 'seedrandom'; +import shader from './tearing.glsl'; +import type { ImageEffectorUiDefinition } from '../image-effector/ImageEffector.js'; +import { defineImageCompositorFunction } from '@/lib/ImageCompositor.js'; +import { i18n } from '@/i18n.js'; + +export const fn = defineImageCompositorFunction<{ + amount: number; + strength: number; + size: number; + channelShift: number; + seed: number; +}>({ + shader, + main: ({ gl, program, u, params }) => { + gl.uniform1i(u.amount, params.amount); + gl.uniform1f(u.channelShift, params.channelShift); + + const rnd = seedrandom(params.seed.toString()); + + for (let i = 0; i < params.amount; i++) { + const o = gl.getUniformLocation(program, `u_shiftOrigins[${i.toString()}]`); + gl.uniform1f(o, rnd()); + + const s = gl.getUniformLocation(program, `u_shiftStrengths[${i.toString()}]`); + gl.uniform1f(s, (1 - (rnd() * 2)) * params.strength); + + const h = gl.getUniformLocation(program, `u_shiftHeights[${i.toString()}]`); + gl.uniform1f(h, rnd() * params.size); + } + }, +}); + +export const uiDefinition = { + name: i18n.ts._imageEffector._fxs.glitch + ': ' + i18n.ts._imageEffector._fxs.tearing, + params: { + amount: { + label: i18n.ts._imageEffector._fxProps.amount, + type: 'number', + default: 3, + min: 1, + max: 100, + step: 1, + }, + strength: { + label: i18n.ts._imageEffector._fxProps.strength, + type: 'number', + default: 0.05, + min: -1, + max: 1, + step: 0.01, + toViewValue: v => Math.round(v * 100) + '%', + }, + size: { + label: i18n.ts._imageEffector._fxProps.size, + type: 'number', + default: 0.2, + min: 0, + max: 1, + step: 0.01, + toViewValue: v => Math.round(v * 100) + '%', + }, + channelShift: { + label: i18n.ts._imageEffector._fxProps.glitchChannelShift, + type: 'number', + default: 0.5, + min: 0, + max: 10, + step: 0.01, + toViewValue: v => Math.round(v * 100) + '%', + }, + seed: { + label: i18n.ts._imageEffector._fxProps.seed, + type: 'seed', + default: 100, + }, + }, +} satisfies ImageEffectorUiDefinition<typeof fn>; diff --git a/packages/frontend/src/utility/image-compositor-functions/threshold.glsl b/packages/frontend/src/utility/image-compositor-functions/threshold.glsl new file mode 100644 index 0000000000..5ca8c46c39 --- /dev/null +++ b/packages/frontend/src/utility/image-compositor-functions/threshold.glsl @@ -0,0 +1,23 @@ +#version 300 es +precision mediump float; + +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +in vec2 in_uv; +uniform sampler2D in_texture; +uniform vec2 in_resolution; +uniform float u_r; +uniform float u_g; +uniform float u_b; +out vec4 out_color; + +void main() { + vec4 in_color = texture(in_texture, in_uv); + float r = in_color.r < u_r ? 0.0 : 1.0; + float g = in_color.g < u_g ? 0.0 : 1.0; + float b = in_color.b < u_b ? 0.0 : 1.0; + out_color = vec4(r, g, b, in_color.a); +} diff --git a/packages/frontend/src/utility/image-compositor-functions/threshold.ts b/packages/frontend/src/utility/image-compositor-functions/threshold.ts new file mode 100644 index 0000000000..83ea788771 --- /dev/null +++ b/packages/frontend/src/utility/image-compositor-functions/threshold.ts @@ -0,0 +1,52 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import shader from './threshold.glsl'; +import type { ImageEffectorUiDefinition } from '../image-effector/ImageEffector.js'; +import { defineImageCompositorFunction } from '@/lib/ImageCompositor.js'; +import { i18n } from '@/i18n.js'; + +export const fn = defineImageCompositorFunction<{ + r: number; + g: number; + b: number; +}>({ + shader, + main: ({ gl, u, params }) => { + gl.uniform1f(u.r, params.r); + gl.uniform1f(u.g, params.g); + gl.uniform1f(u.b, params.b); + }, +}); + +export const uiDefinition = { + name: i18n.ts._imageEffector._fxs.threshold, + params: { + r: { + label: i18n.ts._imageEffector._fxProps.redComponent, + type: 'number', + default: 0.5, + min: 0.0, + max: 1.0, + step: 0.01, + }, + g: { + label: i18n.ts._imageEffector._fxProps.greenComponent, + type: 'number', + default: 0.5, + min: 0.0, + max: 1.0, + step: 0.01, + }, + b: { + label: i18n.ts._imageEffector._fxProps.blueComponent, + type: 'number', + default: 0.5, + min: 0.0, + max: 1.0, + step: 0.01, + }, + }, +} satisfies ImageEffectorUiDefinition<typeof fn>; diff --git a/packages/frontend/src/utility/image-compositor-functions/zoomLines.glsl b/packages/frontend/src/utility/image-compositor-functions/zoomLines.glsl new file mode 100644 index 0000000000..a0f11fcb5b --- /dev/null +++ b/packages/frontend/src/utility/image-compositor-functions/zoomLines.glsl @@ -0,0 +1,48 @@ +#version 300 es +precision mediump float; + +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +// エイリアスを解決してくれないので、プロジェクトルートからの絶対パスにする必要がある +#include /src/shaders/snoise; + +in vec2 in_uv; +uniform sampler2D in_texture; +uniform vec2 in_resolution; +uniform vec2 u_pos; +uniform float u_frequency; +uniform bool u_thresholdEnabled; +uniform float u_threshold; +uniform float u_maskSize; +uniform bool u_black; +out vec4 out_color; + +void main() { + vec4 in_color = texture(in_texture, in_uv); + vec2 centeredUv = (in_uv - vec2(0.5, 0.5)); + vec2 uv = centeredUv; + + float seed = 1.0; + float time = 0.0; + + vec2 noiseUV = (uv - u_pos) / distance((uv - u_pos), vec2(0.0)); + float noiseX = (noiseUV.x + seed) * u_frequency; + float noiseY = (noiseUV.y + seed) * u_frequency; + float noise = (1.0 + snoise(vec3(noiseX, noiseY, time))) / 2.0; + + float t = noise; + if (u_thresholdEnabled) t = t < u_threshold ? 1.0 : 0.0; + + // TODO: マスクの形自体も揺らぎを与える + float d = distance(uv * vec2(2.0, 2.0), u_pos * vec2(2.0, 2.0)); + float mask = d < u_maskSize ? 0.0 : ((d - u_maskSize) * (1.0 + (u_maskSize * 2.0))); + out_color = vec4( + mix(in_color.r, u_black ? 0.0 : 1.0, t * mask), + mix(in_color.g, u_black ? 0.0 : 1.0, t * mask), + mix(in_color.b, u_black ? 0.0 : 1.0, t * mask), + in_color.a + ); +} diff --git a/packages/frontend/src/utility/image-compositor-functions/zoomLines.ts b/packages/frontend/src/utility/image-compositor-functions/zoomLines.ts new file mode 100644 index 0000000000..f8768e4ec3 --- /dev/null +++ b/packages/frontend/src/utility/image-compositor-functions/zoomLines.ts @@ -0,0 +1,87 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import shader from './zoomLines.glsl'; +import type { ImageEffectorUiDefinition } from '../image-effector/ImageEffector.js'; +import { defineImageCompositorFunction } from '@/lib/ImageCompositor.js'; +import { i18n } from '@/i18n.js'; + +export const fn = defineImageCompositorFunction<{ + x: number; + y: number; + frequency: number; + smoothing: boolean; + threshold: number; + maskSize: number; + black: boolean; +}>({ + shader, + main: ({ gl, u, params }) => { + gl.uniform2f(u.pos, params.x / 2, params.y / 2); + gl.uniform1f(u.frequency, params.frequency * params.frequency); + // thresholdの調整が有効な間はsmoothingが利用できない + gl.uniform1i(u.thresholdEnabled, params.smoothing ? 0 : 1); + gl.uniform1f(u.threshold, params.threshold); + gl.uniform1f(u.maskSize, params.maskSize); + gl.uniform1i(u.black, params.black ? 1 : 0); + }, +}); + +export const uiDefinition = { + name: i18n.ts._imageEffector._fxs.zoomLines, + params: { + x: { + label: i18n.ts._imageEffector._fxProps.centerX, + type: 'number', + default: 0.0, + min: -1.0, + max: 1.0, + step: 0.01, + }, + y: { + label: i18n.ts._imageEffector._fxProps.centerY, + type: 'number', + default: 0.0, + min: -1.0, + max: 1.0, + step: 0.01, + }, + frequency: { + label: i18n.ts._imageEffector._fxProps.frequency, + type: 'number', + default: 5.0, + min: 0.0, + max: 15.0, + step: 0.1, + }, + smoothing: { + label: i18n.ts._imageEffector._fxProps.zoomLinesSmoothing, + caption: i18n.ts._imageEffector._fxProps.zoomLinesSmoothingDescription, + type: 'boolean', + default: false, + }, + threshold: { + label: i18n.ts._imageEffector._fxProps.zoomLinesThreshold, + type: 'number', + default: 0.5, + min: 0.0, + max: 1.0, + step: 0.01, + }, + maskSize: { + label: i18n.ts._imageEffector._fxProps.zoomLinesMaskSize, + type: 'number', + default: 0.5, + min: 0.0, + max: 1.0, + step: 0.01, + }, + black: { + label: i18n.ts._imageEffector._fxProps.zoomLinesBlack, + type: 'boolean', + default: false, + }, + }, +} satisfies ImageEffectorUiDefinition<typeof fn>; |