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; "title": string;
"_fxs": {
/**
*
*/
"chromaticAberration": string;
/**
*
*/
"glitch": string;
/**
*
*/
"mirror": string;
};
}; };
} }
declare const locales: { declare const locales: {

View File

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

View File

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

View File

@ -61,7 +61,7 @@ const props = defineProps<{
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'ok', preset: WatermarkPreset): void; (ev: 'ok', image: File): void;
(ev: 'cancel'): void; (ev: 'cancel'): void;
(ev: 'closed'): void; (ev: 'closed'): void;
}>(); }>();
@ -83,7 +83,7 @@ watch(layers, async () => {
function addEffect(ev: MouseEvent) { function addEffect(ev: MouseEvent) {
os.popupMenu(FXS.filter(fx => fx.id !== 'watermarkPlacement').map((fx) => ({ os.popupMenu(FXS.filter(fx => fx.id !== 'watermarkPlacement').map((fx) => ({
text: fx.id, text: fx.name,
action: () => { action: () => {
layers.push({ layers.push({
id: uuid(), id: uuid(),
@ -125,6 +125,14 @@ onUnmounted(() => {
renderer = null; 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> </script>
<style module> <style module>

View File

@ -280,10 +280,14 @@ function showMenu(ev: MouseEvent, item: typeof items.value[0]) {
action: async () => { action: async () => {
const cropped = await os.cropImageFile(item.file, { aspectRatio: null }); const cropped = await os.cropImageFile(item.file, { aspectRatio: null });
URL.revokeObjectURL(item.thumbnail); URL.revokeObjectURL(item.thumbnail);
items.value.splice(items.value.indexOf(item), 1, { const newItem = {
...item, ...item,
file: markRaw(cropped), file: markRaw(cropped),
thumbnail: window.URL.createObjectURL(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')), { const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkImageEffectorDialog.vue')), {
image: img, 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 { getProxiedImageUrl } from '../media-proxy.js';
import { FX_chromaticAberration } from './fxs/chromaticAberration.js'; import { FX_chromaticAberration } from './fxs/chromaticAberration.js';
import { FX_glitch } from './fxs/glitch.js'; import { FX_glitch } from './fxs/glitch.js';
import { FX_mirror } from './fxs/mirror.js';
import { FX_watermarkPlacement } from './fxs/watermarkPlacement.js'; import { FX_watermarkPlacement } from './fxs/watermarkPlacement.js';
type ParamTypeToPrimitive = { type ParamTypeToPrimitive = {
'number': number; 'number': number;
'number:enum': number;
'boolean': boolean; 'boolean': boolean;
'align': { x: 'left' | 'center' | 'right'; y: 'top' | 'center' | 'bottom'; }; 'align': { x: 'left' | 'center' | 'right'; y: 'top' | 'center' | 'bottom'; };
'seed': number; 'seed': number;
@ -26,6 +28,7 @@ export function defineImageEffectorFx<ID extends string, P extends ImageEffector
export type ImageEffectorFx<ID extends string, P extends ImageEffectorFxParamDefs> = { export type ImageEffectorFx<ID extends string, P extends ImageEffectorFxParamDefs> = {
id: ID; id: ID;
name: string;
shader: string; shader: string;
params: P, params: P,
main: (ctx: { main: (ctx: {
@ -49,6 +52,7 @@ export const FXS = [
FX_watermarkPlacement, FX_watermarkPlacement,
FX_chromaticAberration, FX_chromaticAberration,
FX_glitch, FX_glitch,
FX_mirror,
] as const satisfies ImageEffectorFx<string, any>[]; ] as const satisfies ImageEffectorFx<string, any>[];
export type ImageEffectorLayerOf< export type ImageEffectorLayerOf<

View File

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

View File

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