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>
<div :class="$style.root" class="_gaps">
<template v-if="layer.type === 'text'">
<MkInput v-model="layer.text">
<template #label>{{ i18n.ts._watermarkEditor.text }}</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>
<div v-for="[k, v] in Object.entries(fx.params)" :key="k">
<MkSwitch v-if="v.type === 'boolean'" v-model="layer.params[k]">
<template #label>{{ k }}</template>
</MkSwitch>
</template>
<template v-else-if="layer.type === 'image'">
<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 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>
<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>
@ -93,7 +21,7 @@ import { ref, useTemplateRef, watch, onMounted, onUnmounted } from 'vue';
import { v4 as uuid } from 'uuid';
import type { ImageEffectorLayer } from '@/utility/image-effector/ImageEffector.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 MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
@ -107,37 +35,11 @@ import { misskeyApi } from '@/utility/misskey-api.js';
import { prefer } from '@/preferences.js';
const layer = defineModel<ImageEffectorLayer>('layer', { required: true });
const driveFile = ref();
const driveFileError = ref(false);
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;
});
const fx = FXS.find((fx) => fx.id === layer.value.fxId);
if (fx == null) {
throw new Error(`Unrecognized effect: ${layer.value.fxId}`);
}
</script>
<style module>

View File

@ -46,12 +46,12 @@ import { v4 as uuid } from 'uuid';
import type { WatermarkPreset } from '@/utility/watermark.js';
import type { ImageEffectorLayer } from '@/utility/image-effector/ImageEffector.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 MkSelect from '@/components/MkSelect.vue';
import MkButton from '@/components/MkButton.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 { deepClone } from '@/utility/clone.js';
@ -81,7 +81,16 @@ watch(layers, async () => {
}, { deep: true });
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');

View File

@ -43,7 +43,7 @@ export type ImageEffectorFx<ID extends string, P extends ImageEffectorFxParamDef
}) => void;
};
const FXS = [
export const FXS = [
FX_watermarkPlacement,
FX_chromaticAberration,
] as const satisfies ImageEffectorFx<string, any>[];
@ -75,11 +75,11 @@ export class ImageEffector {
private originalImage: ImageData | ImageBitmap | HTMLImageElement | HTMLCanvasElement;
private layers: ImageEffectorLayer[];
private originalImageTexture: WebGLTexture;
private resultTexture: WebGLTexture;
private resultFrameBuffer: WebGLFramebuffer;
private bakedTexturesForWatermarkFx: Map<string, { texture: WebGLTexture; width: number; height: number; }> = new Map();
private texturesKey: string;
private shaderCache: Map<string, WebGLProgram> = new Map();
private perLayerResultTextures: Map<string, WebGLTexture> = new Map();
private perLayerResultFrameBuffers: Map<string, WebGLFramebuffer> = new Map();
constructor(options: {
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.bindTexture(gl.TEXTURE_2D, null);
this.resultTexture = this.createTexture();
this.resultFrameBuffer = gl.createFramebuffer()!;
this.renderTextureProgram = this.initShaderProgram(`#version 300 es
in vec2 position;
out vec2 in_uv;
@ -366,17 +363,6 @@ export class ImageEffector {
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.bindTexture(gl.TEXTURE_2D, this.originalImageTexture);
@ -395,8 +381,31 @@ export class ImageEffector {
// --------------------
let preTexture = this.originalImageTexture;
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.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, this.resultTexture);
gl.bindTexture(gl.TEXTURE_2D, preTexture);
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
@ -431,12 +440,21 @@ export class ImageEffector {
for (const shader of this.shaderCache.values()) {
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();
gl.deleteProgram(this.renderTextureProgram);
gl.deleteProgram(this.renderInvertedTextureProgram);
gl.deleteTexture(this.originalImageTexture);
gl.deleteTexture(this.resultTexture);
gl.deleteFramebuffer(this.resultFrameBuffer);
}
}