This commit is contained in:
syuilo 2025-05-28 17:19:19 +09:00
parent 31c4237748
commit bd8d0d78bf
3 changed files with 62 additions and 133 deletions

View File

@ -5,86 +5,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<div :class="$style.root" class="_gaps"> <div :class="$style.root" class="_gaps">
<template v-if="layer.type === 'text'"> <div v-for="[k, v] in Object.entries(fx.params)" :key="k">
<MkInput v-model="layer.text"> <MkSwitch v-if="v.type === 'boolean'" v-model="layer.params[k]">
<template #label>{{ i18n.ts._watermarkEditor.text }}</template> <template #label>{{ k }}</template>
</MkInput>
<FormSlot>
<template #label>{{ i18n.ts._watermarkEditor.position }}</template>
<MkPositionSelector
v-model:x="layer.alignX"
v-model:y="layer.alignY"
></MkPositionSelector>
</FormSlot>
<MkRange
v-model="layer.scale"
:min="0"
:max="1"
:step="0.01"
:textConverter="(v) => (v * 100).toFixed(1) + '%'"
continuousUpdate
>
<template #label>{{ i18n.ts._watermarkEditor.scale }}</template>
</MkRange>
<MkRange
v-model="layer.opacity"
:min="0"
:max="1"
:step="0.01"
:textConverter="(v) => (v * 100).toFixed(1) + '%'"
continuousUpdate
>
<template #label>{{ i18n.ts._watermarkEditor.opacity }}</template>
</MkRange>
<MkSwitch v-model="layer.repeat">
<template #label>{{ i18n.ts._watermarkEditor.repeat }}</template>
</MkSwitch> </MkSwitch>
</template> <MkRange v-else-if="v.type === 'number'" v-model="layer.params[k]" continuousUpdate :min="v.min" :max="v.max" :step="v.step">
<template v-else-if="layer.type === 'image'"> <template #label>{{ k }}</template>
<MkButton inline rounded primary @click="chooseFile">{{ i18n.ts.selectFile }}</MkButton>
<FormSlot>
<template #label>{{ i18n.ts._watermarkEditor.position }}</template>
<MkPositionSelector
v-model:x="layer.alignX"
v-model:y="layer.alignY"
></MkPositionSelector>
</FormSlot>
<MkRange
v-model="layer.scale"
:min="0"
:max="1"
:step="0.01"
:textConverter="(v) => (v * 100).toFixed(1) + '%'"
continuousUpdate
>
<template #label>{{ i18n.ts._watermarkEditor.scale }}</template>
</MkRange> </MkRange>
</div>
<MkRange
v-model="layer.opacity"
:min="0"
:max="1"
:step="0.01"
:textConverter="(v) => (v * 100).toFixed(1) + '%'"
continuousUpdate
>
<template #label>{{ i18n.ts._watermarkEditor.opacity }}</template>
</MkRange>
<MkSwitch v-model="layer.repeat">
<template #label>{{ i18n.ts._watermarkEditor.repeat }}</template>
</MkSwitch>
<MkSwitch v-model="layer.cover">
<template #label>{{ i18n.ts._watermarkEditor.cover }}</template>
</MkSwitch>
</template>
</div> </div>
</template> </template>
@ -93,7 +21,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 { ImageEffector } from '@/utility/image-effector/ImageEffector.js'; import { FXS, ImageEffector } from '@/utility/image-effector/ImageEffector.js';
import MkSelect from '@/components/MkSelect.vue'; import MkSelect from '@/components/MkSelect.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';
@ -107,37 +35,11 @@ import { misskeyApi } from '@/utility/misskey-api.js';
import { prefer } from '@/preferences.js'; import { prefer } from '@/preferences.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 driveFile = ref(); if (fx == null) {
const driveFileError = ref(false); throw new Error(`Unrecognized effect: ${layer.value.fxId}`);
onMounted(async () => {
if (layer.value.type === 'image' && layer.value.imageId != null) {
await misskeyApi('drive/files/show', {
fileId: layer.value.imageId,
}).then((res) => {
driveFile.value = res;
}).catch((err) => {
driveFileError.value = true;
});
}
});
function chooseFile(ev: MouseEvent) {
selectFile(ev.currentTarget ?? ev.target, i18n.ts.selectFile).then((file) => {
if (!file.type.startsWith('image')) {
os.alert({
type: 'warning',
title: i18n.ts._watermarkEditor.driveFileTypeWarn,
text: i18n.ts._watermarkEditor.driveFileTypeWarnDescription,
});
return;
}
layer.value.imageId = file.id;
layer.value.imageUrl = file.url;
driveFileError.value = false;
});
} }
</script> </script>
<style module> <style module>

View File

@ -46,12 +46,12 @@ 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 { ImageEffector } from '@/utility/image-effector/ImageEffector.js'; import { FXS, 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';
import MkInput from '@/components/MkInput.vue'; import MkInput from '@/components/MkInput.vue';
import XLayer from '@/components/MkWatermarkEditorDialog.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';
@ -81,7 +81,16 @@ watch(layers, async () => {
}, { deep: true }); }, { deep: true });
function addEffect(ev: MouseEvent) { function addEffect(ev: MouseEvent) {
os.popupMenu(FXS.map((fx) => ({
text: fx.id,
action: () => {
layers.push({
id: uuid(),
fxId: fx.id,
params: Object.fromEntries(Object.entries(fx.params).map(([k, v]) => [k, v.default])),
});
},
})), ev.currentTarget ?? ev.target);
} }
const canvasEl = useTemplateRef('canvasEl'); const canvasEl = useTemplateRef('canvasEl');

View File

@ -43,7 +43,7 @@ export type ImageEffectorFx<ID extends string, P extends ImageEffectorFxParamDef
}) => void; }) => void;
}; };
const FXS = [ export const FXS = [
FX_watermarkPlacement, FX_watermarkPlacement,
FX_chromaticAberration, FX_chromaticAberration,
] as const satisfies ImageEffectorFx<string, any>[]; ] as const satisfies ImageEffectorFx<string, any>[];
@ -75,11 +75,11 @@ export class ImageEffector {
private originalImage: ImageData | ImageBitmap | HTMLImageElement | HTMLCanvasElement; private originalImage: ImageData | ImageBitmap | HTMLImageElement | HTMLCanvasElement;
private layers: ImageEffectorLayer[]; private layers: ImageEffectorLayer[];
private originalImageTexture: WebGLTexture; private originalImageTexture: WebGLTexture;
private resultTexture: WebGLTexture;
private resultFrameBuffer: WebGLFramebuffer;
private bakedTexturesForWatermarkFx: Map<string, { texture: WebGLTexture; width: number; height: number; }> = new Map(); private bakedTexturesForWatermarkFx: Map<string, { texture: WebGLTexture; width: number; height: number; }> = new Map();
private texturesKey: string; private texturesKey: string;
private shaderCache: Map<string, WebGLProgram> = new Map(); private shaderCache: Map<string, WebGLProgram> = new Map();
private perLayerResultTextures: Map<string, WebGLTexture> = new Map();
private perLayerResultFrameBuffers: Map<string, WebGLFramebuffer> = new Map();
constructor(options: { constructor(options: {
canvas: HTMLCanvasElement; canvas: HTMLCanvasElement;
@ -118,9 +118,6 @@ export class ImageEffector {
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, options.width, options.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, this.originalImage); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, options.width, options.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, this.originalImage);
gl.bindTexture(gl.TEXTURE_2D, null); gl.bindTexture(gl.TEXTURE_2D, null);
this.resultTexture = this.createTexture();
this.resultFrameBuffer = gl.createFramebuffer()!;
this.renderTextureProgram = this.initShaderProgram(`#version 300 es this.renderTextureProgram = this.initShaderProgram(`#version 300 es
in vec2 position; in vec2 position;
out vec2 in_uv; out vec2 in_uv;
@ -366,17 +363,6 @@ export class ImageEffector {
throw new Error('gl is not initialized'); throw new Error('gl is not initialized');
} }
gl.bindTexture(gl.TEXTURE_2D, this.resultTexture);
gl.texImage2D(
gl.TEXTURE_2D, 0, gl.RGBA, this.renderWidth, this.renderHeight, 0,
gl.RGBA, gl.UNSIGNED_BYTE, null);
gl.bindTexture(gl.TEXTURE_2D, null);
gl.bindFramebuffer(gl.FRAMEBUFFER, this.resultFrameBuffer);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.resultTexture, 0);
// --------------------
{ {
gl.activeTexture(gl.TEXTURE0); gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, this.originalImageTexture); gl.bindTexture(gl.TEXTURE_2D, this.originalImageTexture);
@ -395,8 +381,31 @@ export class ImageEffector {
// -------------------- // --------------------
let preTexture = this.originalImageTexture;
for (const layer of this.layers) { for (const layer of this.layers) {
this.renderLayer(layer, this.originalImageTexture); const cachedResultTexture = this.perLayerResultTextures.get(layer.id);
const resultTexture = cachedResultTexture ?? this.createTexture();
if (cachedResultTexture == null) {
this.perLayerResultTextures.set(layer.id, resultTexture);
}
gl.bindTexture(gl.TEXTURE_2D, resultTexture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, this.renderWidth, this.renderHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
gl.bindTexture(gl.TEXTURE_2D, null);
const cachedResultFrameBuffer = this.perLayerResultFrameBuffers.get(layer.id);
const resultFrameBuffer = cachedResultFrameBuffer ?? gl.createFramebuffer()!;
if (cachedResultFrameBuffer == null) {
this.perLayerResultFrameBuffers.set(layer.id, resultFrameBuffer);
}
gl.bindFramebuffer(gl.FRAMEBUFFER, resultFrameBuffer);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, resultTexture, 0);
this.renderLayer(layer, preTexture);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
preTexture = resultTexture;
} }
// -------------------- // --------------------
@ -405,7 +414,7 @@ export class ImageEffector {
gl.useProgram(this.renderInvertedTextureProgram); gl.useProgram(this.renderInvertedTextureProgram);
gl.activeTexture(gl.TEXTURE0); gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, this.resultTexture); gl.bindTexture(gl.TEXTURE_2D, preTexture);
gl.drawArrays(gl.TRIANGLES, 0, 6); gl.drawArrays(gl.TRIANGLES, 0, 6);
} }
@ -431,12 +440,21 @@ export class ImageEffector {
for (const shader of this.shaderCache.values()) { for (const shader of this.shaderCache.values()) {
gl.deleteProgram(shader); gl.deleteProgram(shader);
} }
this.shaderCache.clear();
for (const texture of this.perLayerResultTextures.values()) {
gl.deleteTexture(texture);
}
this.perLayerResultTextures.clear();
for (const framebuffer of this.perLayerResultFrameBuffers.values()) {
gl.deleteFramebuffer(framebuffer);
}
this.perLayerResultFrameBuffers.clear();
this.disposeBakedTextures(); this.disposeBakedTextures();
gl.deleteProgram(this.renderTextureProgram); gl.deleteProgram(this.renderTextureProgram);
gl.deleteProgram(this.renderInvertedTextureProgram); gl.deleteProgram(this.renderInvertedTextureProgram);
gl.deleteTexture(this.originalImageTexture); gl.deleteTexture(this.originalImageTexture);
gl.deleteTexture(this.resultTexture);
gl.deleteFramebuffer(this.resultFrameBuffer);
} }
} }