wip
This commit is contained in:
parent
31c4237748
commit
bd8d0d78bf
|
@ -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;
|
||||
const fx = FXS.find((fx) => fx.id === layer.value.fxId);
|
||||
if (fx == null) {
|
||||
throw new Error(`Unrecognized effect: ${layer.value.fxId}`);
|
||||
}
|
||||
|
||||
layer.value.imageId = file.id;
|
||||
layer.value.imageUrl = file.url;
|
||||
driveFileError.value = false;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style module>
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue