This commit is contained in:
syuilo 2025-05-29 09:36:47 +09:00
parent 16a3321287
commit 98f0de6c56
12 changed files with 277 additions and 26 deletions

12
locales/index.d.ts vendored
View File

@ -12092,6 +12092,18 @@ export interface Locale extends ILocale {
* *
*/ */
"invert": string; "invert": string;
/**
*
*/
"grayscale": string;
/**
*
*/
"colorClamp": string;
/**
* ()
*/
"colorClampAdvanced": string;
}; };
}; };
} }

View File

@ -3241,3 +3241,6 @@ _imageEffector:
glitch: "グリッチ" glitch: "グリッチ"
mirror: "ミラー" mirror: "ミラー"
invert: "色の反転" invert: "色の反転"
grayscale: "白黒"
colorClamp: "色の圧縮"
colorClampAdvanced: "色の圧縮(高度)"

View File

@ -37,7 +37,7 @@ import { ref, useTemplateRef, watch, onMounted, onUnmounted } from 'vue';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import type { ImageEffectorLayer } from '@/utility/image-effector/ImageEffector.js'; import type { ImageEffectorLayer } from '@/utility/image-effector/ImageEffector.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { FXS, ImageEffector } from '@/utility/image-effector/ImageEffector.js'; import { ImageEffector } from '@/utility/image-effector/ImageEffector.js';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue'; import MkInput from '@/components/MkInput.vue';
@ -50,6 +50,7 @@ import * as os from '@/os.js';
import { selectFile } from '@/utility/drive.js'; import { selectFile } from '@/utility/drive.js';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
import { prefer } from '@/preferences.js'; import { prefer } from '@/preferences.js';
import { FXS } from '@/utility/image-effector/fxs.js';
const layer = defineModel<ImageEffectorLayer>('layer', { required: true }); const layer = defineModel<ImageEffectorLayer>('layer', { required: true });
const fx = FXS.find((fx) => fx.id === layer.value.fxId); const fx = FXS.find((fx) => fx.id === layer.value.fxId);

View File

@ -23,6 +23,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.previewContainer"> <div :class="$style.previewContainer">
<div class="_acrylic" :class="$style.previewTitle">{{ i18n.ts.preview }}</div> <div class="_acrylic" :class="$style.previewTitle">{{ i18n.ts.preview }}</div>
<div class="_acrylic" :class="$style.previewControls"> <div class="_acrylic" :class="$style.previewControls">
<button class="_button" :class="[$style.previewControlsButton, !enabled ? $style.active : null]" @click="enabled = false">Before</button>
<button class="_button" :class="[$style.previewControlsButton, enabled ? $style.active : null]" @click="enabled = true">After</button>
</div> </div>
</div> </div>
</div> </div>
@ -49,7 +51,7 @@ import { v4 as uuid } from 'uuid';
import type { WatermarkPreset } from '@/utility/watermark.js'; import type { WatermarkPreset } from '@/utility/watermark.js';
import type { ImageEffectorLayer } from '@/utility/image-effector/ImageEffector.js'; import type { ImageEffectorLayer } from '@/utility/image-effector/ImageEffector.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { FXS, ImageEffector } from '@/utility/image-effector/ImageEffector.js'; import { ImageEffector } from '@/utility/image-effector/ImageEffector.js';
import MkModalWindow from '@/components/MkModalWindow.vue'; import MkModalWindow from '@/components/MkModalWindow.vue';
import MkSelect from '@/components/MkSelect.vue'; import MkSelect from '@/components/MkSelect.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
@ -57,6 +59,7 @@ import MkInput from '@/components/MkInput.vue';
import XLayer from '@/components/MkImageEffectorDialog.Layer.vue'; import XLayer from '@/components/MkImageEffectorDialog.Layer.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { deepClone } from '@/utility/clone.js'; import { deepClone } from '@/utility/clone.js';
import { FXS } from '@/utility/image-effector/fxs.js';
const props = defineProps<{ const props = defineProps<{
image: HTMLImageElement; image: HTMLImageElement;
@ -114,6 +117,7 @@ onMounted(async () => {
height: props.image.height, height: props.image.height,
layers: layers, layers: layers,
originalImage: props.image, originalImage: props.image,
fxs: FXS,
}); });
await renderer!.bakeTextures(); await renderer!.bakeTextures();
@ -135,6 +139,18 @@ function save() {
dialog.value?.close(); dialog.value?.close();
}, 'image/png'); }, 'image/png');
} }
const enabled = ref(true);
watch(enabled, () => {
if (renderer != null) {
if (enabled.value) {
renderer.updateLayers(layers);
} else {
renderer.updateLayers([]);
}
renderer.render();
}
});
</script> </script>
<style module> <style module>

View File

@ -98,6 +98,7 @@ import * as os from '@/os.js';
import { ensureSignin } from '@/i.js'; import { ensureSignin } from '@/i.js';
import { ImageEffector } from '@/utility/image-effector/ImageEffector.js'; import { ImageEffector } from '@/utility/image-effector/ImageEffector.js';
import { makeImageEffectorLayers } from '@/utility/watermark.js'; import { makeImageEffectorLayers } from '@/utility/watermark.js';
import { FX_watermarkPlacement } from '@/utility/image-effector/fxs/watermarkPlacement.js';
const $i = ensureSignin(); const $i = ensureSignin();
@ -527,6 +528,7 @@ async function preprocess(item: (typeof items)['value'][number]): Promise<void>
height: img.height, height: img.height,
layers: makeImageEffectorLayers(preset.layers), layers: makeImageEffectorLayers(preset.layers),
originalImage: img, originalImage: img,
fxs: [FX_watermarkPlacement],
}); });
await renderer.bakeTextures(); await renderer.bakeTextures();

View File

@ -59,6 +59,7 @@ import XLayer from '@/components/MkWatermarkEditorDialog.Layer.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { deepClone } from '@/utility/clone.js'; import { deepClone } from '@/utility/clone.js';
import { ensureSignin } from '@/i.js'; import { ensureSignin } from '@/i.js';
import { FX_watermarkPlacement } from '@/utility/image-effector/fxs/watermarkPlacement.js';
const $i = ensureSignin(); const $i = ensureSignin();
@ -160,6 +161,7 @@ async function initRenderer() {
height: 1000, height: 1000,
layers: makeImageEffectorLayers(preset.layers), layers: makeImageEffectorLayers(preset.layers),
originalImage: sampleImage_3_2, originalImage: sampleImage_3_2,
fxs: [FX_watermarkPlacement],
}); });
} else if (sampleImageType.value === '2_3') { } else if (sampleImageType.value === '2_3') {
renderer = new ImageEffector({ renderer = new ImageEffector({
@ -168,6 +170,7 @@ async function initRenderer() {
height: 1500, height: 1500,
layers: makeImageEffectorLayers(preset.layers), layers: makeImageEffectorLayers(preset.layers),
originalImage: sampleImage_2_3, originalImage: sampleImage_2_3,
fxs: [FX_watermarkPlacement],
}); });
} }

View File

@ -30,6 +30,7 @@ import { i18n } from '@/i18n.js';
import { deepClone } from '@/utility/clone.js'; import { deepClone } from '@/utility/clone.js';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import { ImageEffector } from '@/utility/image-effector/ImageEffector.js'; import { ImageEffector } from '@/utility/image-effector/ImageEffector.js';
import { FX_watermarkPlacement } from '@/utility/image-effector/fxs/watermarkPlacement.js';
const props = defineProps<{ const props = defineProps<{
preset: WatermarkPreset; preset: WatermarkPreset;
@ -78,6 +79,7 @@ onMounted(() => {
height: 1000, height: 1000,
layers: makeImageEffectorLayers(props.preset.layers), layers: makeImageEffectorLayers(props.preset.layers),
originalImage: sampleImage, originalImage: sampleImage,
fxs: [FX_watermarkPlacement],
}); });
await renderer.bakeTextures(); await renderer.bakeTextures();

View File

@ -4,11 +4,6 @@
*/ */
import { getProxiedImageUrl } from '../media-proxy.js'; import { getProxiedImageUrl } from '../media-proxy.js';
import { FX_chromaticAberration } from './fxs/chromaticAberration.js';
import { FX_glitch } from './fxs/glitch.js';
import { FX_invert } from './fxs/invert.js';
import { FX_mirror } from './fxs/mirror.js';
import { FX_watermarkPlacement } from './fxs/watermarkPlacement.js';
type ParamTypeToPrimitive = { type ParamTypeToPrimitive = {
'number': number; 'number': number;
@ -49,31 +44,16 @@ export type ImageEffectorFx<ID extends string, P extends ImageEffectorFxParamDef
}) => void; }) => void;
}; };
export const FXS = [ export type ImageEffectorLayer = {
FX_watermarkPlacement,
FX_chromaticAberration,
FX_glitch,
FX_mirror,
FX_invert,
] as const satisfies ImageEffectorFx<string, any>[];
export type ImageEffectorLayerOf<
FXID extends (typeof FXS)[number]['id'],
FX extends { params: ImageEffectorFxParamDefs } = Extract<(typeof FXS)[number], { id: FXID }>,
> = {
id: string; id: string;
fxId: FXID; fxId: string;
params: { params: Record<string, any>;
[key in keyof FX['params']]: ParamTypeToPrimitive[FX['params'][key]['type']];
};
// for watermarkPlacement fx // for watermarkPlacement fx
imageUrl?: string | null; imageUrl?: string | null;
text?: string | null; text?: string | null;
}; };
export type ImageEffectorLayer = ImageEffectorLayerOf<(typeof FXS)[number]['id'], Extract<(typeof FXS)[number], { id: (typeof FXS)[number]['id'] }>>;
export class ImageEffector { export class ImageEffector {
private canvas: HTMLCanvasElement | null = null; private canvas: HTMLCanvasElement | null = null;
private gl: WebGL2RenderingContext | null = null; private gl: WebGL2RenderingContext | null = null;
@ -89,6 +69,7 @@ export class ImageEffector {
private shaderCache: Map<string, WebGLProgram> = new Map(); private shaderCache: Map<string, WebGLProgram> = new Map();
private perLayerResultTextures: Map<string, WebGLTexture> = new Map(); private perLayerResultTextures: Map<string, WebGLTexture> = new Map();
private perLayerResultFrameBuffers: Map<string, WebGLFramebuffer> = new Map(); private perLayerResultFrameBuffers: Map<string, WebGLFramebuffer> = new Map();
private fxs: ImageEffectorFx<string, any>[];
constructor(options: { constructor(options: {
canvas: HTMLCanvasElement; canvas: HTMLCanvasElement;
@ -96,6 +77,7 @@ export class ImageEffector {
height: number; height: number;
originalImage: ImageData | ImageBitmap | HTMLImageElement | HTMLCanvasElement; originalImage: ImageData | ImageBitmap | HTMLImageElement | HTMLCanvasElement;
layers: ImageEffectorLayer[]; layers: ImageEffectorLayer[];
fxs: ImageEffectorFx<string, any>[];
}) { }) {
this.canvas = options.canvas; this.canvas = options.canvas;
this.canvas.width = options.width; this.canvas.width = options.width;
@ -104,6 +86,7 @@ export class ImageEffector {
this.renderHeight = options.height; this.renderHeight = options.height;
this.originalImage = options.originalImage; this.originalImage = options.originalImage;
this.layers = options.layers; this.layers = options.layers;
this.fxs = options.fxs;
this.texturesKey = this.calcTexturesKey(); this.texturesKey = this.calcTexturesKey();
this.gl = this.canvas.getContext('webgl2', { this.gl = this.canvas.getContext('webgl2', {
@ -328,7 +311,7 @@ export class ImageEffector {
throw new Error('gl is not initialized'); throw new Error('gl is not initialized');
} }
const fx = FXS.find(fx => fx.id === layer.fxId); const fx = this.fxs.find(fx => fx.id === layer.fxId);
if (fx == null) return; if (fx == null) return;
const watermark = layer.fxId === 'watermarkPlacement' ? this.bakedTexturesForWatermarkFx.get(layer.id) : undefined; const watermark = layer.fxId === 'watermarkPlacement' ? this.bakedTexturesForWatermarkFx.get(layer.id) : undefined;

View File

@ -0,0 +1,25 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { FX_chromaticAberration } from './fxs/chromaticAberration.js';
import { FX_colorClamp } from './fxs/colorClamp.js';
import { FX_colorClampAdvanced } from './fxs/colorClampAdvanced.js';
import { FX_glitch } from './fxs/glitch.js';
import { FX_grayscale } from './fxs/grayscale.js';
import { FX_invert } from './fxs/invert.js';
import { FX_mirror } from './fxs/mirror.js';
import { FX_watermarkPlacement } from './fxs/watermarkPlacement.js';
import type { ImageEffectorFx } from './ImageEffector.js';
export const FXS = [
FX_watermarkPlacement,
FX_chromaticAberration,
FX_glitch,
FX_mirror,
FX_invert,
FX_grayscale,
FX_colorClamp,
FX_colorClampAdvanced,
] as const satisfies ImageEffectorFx<string, any>[];

View File

@ -0,0 +1,60 @@
/*
* 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 u_texture;
uniform vec2 u_resolution;
uniform float u_max;
uniform float u_min;
out vec4 out_color;
void main() {
vec4 in_color = texture(u_texture, in_uv);
float r = min(max(in_color.r, u_min), u_max);
float g = min(max(in_color.g, u_min), u_max);
float b = min(max(in_color.b, u_min), u_max);
out_color = vec4(r, g, b, in_color.a);
}
`;
export const FX_colorClamp = defineImageEffectorFx({
id: 'colorClamp' as const,
name: i18n.ts._imageEffector._fxs.colorClamp,
shader,
params: {
max: {
type: 'number' as const,
default: 1.0,
min: 0.0,
max: 1.0,
step: 0.01,
},
min: {
type: 'number' as const,
default: -1.0,
min: -1.0,
max: 0.0,
step: 0.01,
},
},
main: ({ gl, program, params, preTexture }) => {
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, preTexture);
const u_texture = gl.getUniformLocation(program, 'u_texture');
gl.uniform1i(u_texture, 0);
const u_max = gl.getUniformLocation(program, 'u_max');
gl.uniform1f(u_max, params.max);
const u_min = gl.getUniformLocation(program, 'u_min');
gl.uniform1f(u_min, 1.0 + params.min);
},
});

View File

@ -0,0 +1,104 @@
/*
* 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 u_texture;
uniform vec2 u_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(u_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);
}
`;
export const FX_colorClampAdvanced = defineImageEffectorFx({
id: 'colorClampAdvanced' as const,
name: i18n.ts._imageEffector._fxs.colorClampAdvanced,
shader,
params: {
rMax: {
type: 'number' as const,
default: 1.0,
min: 0.0,
max: 1.0,
step: 0.01,
},
rMin: {
type: 'number' as const,
default: -1.0,
min: -1.0,
max: 0.0,
step: 0.01,
},
gMax: {
type: 'number' as const,
default: 1.0,
min: 0.0,
max: 1.0,
step: 0.01,
},
gMin: {
type: 'number' as const,
default: -1.0,
min: -1.0,
max: 0.0,
step: 0.01,
},
bMax: {
type: 'number' as const,
default: 1.0,
min: 0.0,
max: 1.0,
step: 0.01,
},
bMin: {
type: 'number' as const,
default: -1.0,
min: -1.0,
max: 0.0,
step: 0.01,
},
},
main: ({ gl, program, params, preTexture }) => {
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, preTexture);
const u_texture = gl.getUniformLocation(program, 'u_texture');
gl.uniform1i(u_texture, 0);
const u_rMax = gl.getUniformLocation(program, 'u_rMax');
gl.uniform1f(u_rMax, params.rMax);
const u_rMin = gl.getUniformLocation(program, 'u_rMin');
gl.uniform1f(u_rMin, 1.0 + params.rMin);
const u_gMax = gl.getUniformLocation(program, 'u_gMax');
gl.uniform1f(u_gMax, params.gMax);
const u_gMin = gl.getUniformLocation(program, 'u_gMin');
gl.uniform1f(u_gMin, 1.0 + params.gMin);
const u_bMax = gl.getUniformLocation(program, 'u_bMax');
gl.uniform1f(u_bMax, params.bMax);
const u_bMin = gl.getUniformLocation(program, 'u_bMin');
gl.uniform1f(u_bMin, 1.0 + params.bMin);
},
});

View File

@ -0,0 +1,40 @@
/*
* 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 u_texture;
uniform vec2 u_resolution;
out vec4 out_color;
float getBrightness(vec4 color) {
return (color.r + color.g + color.b) / 3.0;
}
void main() {
vec4 in_color = texture(u_texture, in_uv);
float brightness = getBrightness(in_color);
out_color = vec4(brightness, brightness, brightness, in_color.a);
}
`;
export const FX_grayscale = defineImageEffectorFx({
id: 'grayscale' as const,
name: i18n.ts._imageEffector._fxs.grayscale,
shader,
params: {
},
main: ({ gl, program, params, preTexture }) => {
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, preTexture);
const u_texture = gl.getUniformLocation(program, 'u_texture');
gl.uniform1i(u_texture, 0);
},
});