This commit is contained in:
syuilo 2025-05-28 21:00:03 +09:00
parent 9fb0f7357a
commit 65e0ab810e
9 changed files with 129 additions and 6 deletions

14
locales/index.d.ts vendored
View File

@ -12071,6 +12071,20 @@ export interface Locale extends ILocale {
*
*/
"title": string;
"_fxs": {
/**
*
*/
"chromaticAberration": string;
/**
*
*/
"glitch": string;
/**
*
*/
"mirror": string;
};
};
}
declare const locales: {

View File

@ -3234,3 +3234,8 @@ _watermarkEditor:
_imageEffector:
title: "エフェクト"
_fxs:
chromaticAberration: "色収差"
glitch: "グリッチ"
mirror: "ミラー"

View File

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<MkFolder :defaultOpen="true">
<template #label>{{ fx.id }}</template>
<template #label>{{ fx.name }}</template>
<template #footer>
<MkButton @click="emit('del')">{{ i18n.ts.remove }}</MkButton>
</template>
@ -18,6 +18,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkRange v-else-if="v.type === 'number'" v-model="layer.params[k]" continuousUpdate :min="v.min" :max="v.max" :step="v.step">
<template #label>{{ k }}</template>
</MkRange>
<MkRadios v-else-if="v.type === 'number:enum'" v-model="layer.params[k]">
<template #label>{{ k }}</template>
<option v-for="item in v.enum" :value="item.value">{{ item.label }}</option>
</MkRadios>
<div v-else-if="v.type === 'seed'">
<MkRange v-model="layer.params[k]" continuousUpdate type="number" :min="0" :max="10000" :step="1">
<template #label>{{ k }}</template>
@ -37,6 +41,7 @@ import { FXS, ImageEffector } from '@/utility/image-effector/ImageEffector.js';
import MkFolder from '@/components/MkFolder.vue';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkRadios from '@/components/MkRadios.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkRange from '@/components/MkRange.vue';
import FormSlot from '@/components/form/slot.vue';

View File

@ -61,7 +61,7 @@ const props = defineProps<{
}>();
const emit = defineEmits<{
(ev: 'ok', preset: WatermarkPreset): void;
(ev: 'ok', image: File): void;
(ev: 'cancel'): void;
(ev: 'closed'): void;
}>();
@ -83,7 +83,7 @@ watch(layers, async () => {
function addEffect(ev: MouseEvent) {
os.popupMenu(FXS.filter(fx => fx.id !== 'watermarkPlacement').map((fx) => ({
text: fx.id,
text: fx.name,
action: () => {
layers.push({
id: uuid(),
@ -125,6 +125,14 @@ onUnmounted(() => {
renderer = null;
}
});
function save() {
renderer!.render(); // toBlob
canvasEl.value!.toBlob((blob) => {
emit('ok', new File([blob!], `image-${Date.now()}.png`, { type: 'image/png' }));
dialog.value?.close();
}, 'image/png');
}
</script>
<style module>

View File

@ -280,10 +280,14 @@ function showMenu(ev: MouseEvent, item: typeof items.value[0]) {
action: async () => {
const cropped = await os.cropImageFile(item.file, { aspectRatio: null });
URL.revokeObjectURL(item.thumbnail);
items.value.splice(items.value.indexOf(item), 1, {
const newItem = {
...item,
file: markRaw(cropped),
thumbnail: window.URL.createObjectURL(cropped),
};
items.value.splice(items.value.indexOf(item), 1, newItem);
preprocess(newItem).then(() => {
triggerRef(items);
});
},
});
@ -298,9 +302,22 @@ function showMenu(ev: MouseEvent, item: typeof items.value[0]) {
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkImageEffectorDialog.vue')), {
image: img,
}, {
ok: () => {
ok: (file) => {
URL.revokeObjectURL(item.thumbnail);
const newItem = {
...item,
file: markRaw(file),
thumbnail: window.URL.createObjectURL(file),
};
items.value.splice(items.value.indexOf(item), 1, newItem);
preprocess(newItem).then(() => {
triggerRef(items);
});
},
closed: () => {
URL.revokeObjectURL(img.src);
dispose();
},
closed: () => dispose(),
});
},
});

View File

@ -6,10 +6,12 @@
import { getProxiedImageUrl } from '../media-proxy.js';
import { FX_chromaticAberration } from './fxs/chromaticAberration.js';
import { FX_glitch } from './fxs/glitch.js';
import { FX_mirror } from './fxs/mirror.js';
import { FX_watermarkPlacement } from './fxs/watermarkPlacement.js';
type ParamTypeToPrimitive = {
'number': number;
'number:enum': number;
'boolean': boolean;
'align': { x: 'left' | 'center' | 'right'; y: 'top' | 'center' | 'bottom'; };
'seed': number;
@ -26,6 +28,7 @@ export function defineImageEffectorFx<ID extends string, P extends ImageEffector
export type ImageEffectorFx<ID extends string, P extends ImageEffectorFxParamDefs> = {
id: ID;
name: string;
shader: string;
params: P,
main: (ctx: {
@ -49,6 +52,7 @@ export const FXS = [
FX_watermarkPlacement,
FX_chromaticAberration,
FX_glitch,
FX_mirror,
] as const satisfies ImageEffectorFx<string, any>[];
export type ImageEffectorLayerOf<

View File

@ -4,6 +4,7 @@
*/
import { defineImageEffectorFx } from '../ImageEffector.js';
import { i18n } from '@/i18n.js';
const shader = `#version 300 es
precision highp float;
@ -52,6 +53,7 @@ void main() {
export const FX_chromaticAberration = defineImageEffectorFx({
id: 'chromaticAberration' as const,
name: i18n.ts._imageEffector._fxs.chromaticAberration,
shader,
params: {
normalize: {

View File

@ -5,6 +5,7 @@
import seedrandom from 'seedrandom';
import { defineImageEffectorFx } from '../ImageEffector.js';
import { i18n } from '@/i18n.js';
const shader = `#version 300 es
precision highp float;
@ -38,6 +39,7 @@ void main() {
export const FX_glitch = defineImageEffectorFx({
id: 'glitch' as const,
name: i18n.ts._imageEffector._fxs.glitch,
shader,
params: {
amount: {

View File

@ -0,0 +1,66 @@
/*
* 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 highp float;
in vec2 in_uv;
uniform sampler2D u_texture;
uniform vec2 u_resolution;
uniform int u_h;
uniform int u_v;
out vec4 out_color;
void main() {
vec4 pixel = texture(u_texture, in_uv);
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(u_texture, uv);
}
`;
export const FX_mirror = defineImageEffectorFx({
id: 'mirror' as const,
name: i18n.ts._imageEffector._fxs.mirror,
shader,
params: {
h: {
type: 'number:enum' as const,
enum: [{ value: -1, label: '<-' }, { value: 0, label: '|' }, { value: 1, label: '->' }],
default: -1,
},
v: {
type: 'number:enum' as const,
enum: [{ value: -1, label: '^' }, { value: 0, label: '-' }, { value: 1, label: 'v' }],
default: 0,
},
},
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_h = gl.getUniformLocation(program, 'u_h');
gl.uniform1i(u_h, params.h);
const u_v = gl.getUniformLocation(program, 'u_v');
gl.uniform1i(u_v, params.v);
},
});