wip
This commit is contained in:
parent
2d2b9e7a3f
commit
236b8913d2
|
@ -48,7 +48,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script setup lang="ts">
|
||||
import { ref, useTemplateRef, watch, onMounted, onUnmounted, reactive } from 'vue';
|
||||
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';
|
||||
|
@ -82,7 +81,7 @@ const layers = reactive<ImageEffectorLayer[]>([]);
|
|||
|
||||
watch(layers, async () => {
|
||||
if (renderer != null) {
|
||||
renderer.updateLayers(layers);
|
||||
renderer.setLayers(layers);
|
||||
}
|
||||
}, { deep: true });
|
||||
|
||||
|
@ -111,18 +110,19 @@ const canvasEl = useTemplateRef('canvasEl');
|
|||
let renderer: ImageEffector | null = null;
|
||||
|
||||
onMounted(async () => {
|
||||
if (canvasEl.value == null) return;
|
||||
|
||||
renderer = new ImageEffector({
|
||||
canvas: canvasEl.value,
|
||||
width: props.image.width,
|
||||
height: props.image.height,
|
||||
layers: layers,
|
||||
originalImage: props.image,
|
||||
renderWidth: props.image.width,
|
||||
renderHeight: props.image.height,
|
||||
image: props.image,
|
||||
fxs: FXS,
|
||||
});
|
||||
|
||||
await renderer!.bakeTextures();
|
||||
await renderer.setLayers(layers);
|
||||
|
||||
renderer!.render();
|
||||
renderer.render();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
|
@ -149,9 +149,9 @@ const enabled = ref(true);
|
|||
watch(enabled, () => {
|
||||
if (renderer != null) {
|
||||
if (enabled.value) {
|
||||
renderer.updateLayers(layers);
|
||||
renderer.setLayers(layers);
|
||||
} else {
|
||||
renderer.updateLayers([]);
|
||||
renderer.setLayers([]);
|
||||
}
|
||||
renderer.render();
|
||||
}
|
||||
|
|
|
@ -96,9 +96,7 @@ import { isWebpSupported } from '@/utility/isWebpSupported.js';
|
|||
import { uploadFile, UploadAbortedError } from '@/utility/drive.js';
|
||||
import * as os from '@/os.js';
|
||||
import { ensureSignin } from '@/i.js';
|
||||
import { ImageEffector } from '@/utility/image-effector/ImageEffector.js';
|
||||
import { makeImageEffectorLayers } from '@/utility/watermark.js';
|
||||
import { FX_watermarkPlacement } from '@/utility/image-effector/fxs/watermarkPlacement.js';
|
||||
import { WatermarkRenderer } from '@/utility/watermark.js';
|
||||
|
||||
const $i = ensureSignin();
|
||||
|
||||
|
@ -522,16 +520,14 @@ async function preprocess(item: (typeof items)['value'][number]): Promise<void>
|
|||
const preset = prefer.s.watermarkPresets.find(p => p.id === item.watermarkPresetId);
|
||||
if (needsWatermark && preset != null) {
|
||||
const canvas = window.document.createElement('canvas');
|
||||
const renderer = new ImageEffector({
|
||||
const renderer = new WatermarkRenderer({
|
||||
canvas: canvas,
|
||||
width: img.width,
|
||||
height: img.height,
|
||||
layers: makeImageEffectorLayers(preset.layers),
|
||||
originalImage: img,
|
||||
fxs: [FX_watermarkPlacement],
|
||||
renderWidth: img.width,
|
||||
renderHeight: img.height,
|
||||
image: img,
|
||||
});
|
||||
|
||||
await renderer.bakeTextures();
|
||||
await renderer.setLayers(preset.layers);
|
||||
|
||||
renderer.render();
|
||||
|
||||
|
@ -541,6 +537,7 @@ async function preprocess(item: (typeof items)['value'][number]): Promise<void>
|
|||
throw new Error('Failed to convert canvas to blob');
|
||||
}
|
||||
resolve(blob);
|
||||
renderer.destroy();
|
||||
}, 'image/png');
|
||||
});
|
||||
}
|
||||
|
|
|
@ -47,10 +47,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script setup lang="ts">
|
||||
import { ref, useTemplateRef, watch, onMounted, onUnmounted, reactive } from 'vue';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import type { WatermarkPreset } from '@/utility/watermark.js';
|
||||
import { makeImageEffectorLayers } from '@/utility/watermark.js';
|
||||
import { WatermarkRenderer } from '@/utility/watermark.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { 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';
|
||||
|
@ -59,7 +57,6 @@ import XLayer from '@/components/MkWatermarkEditorDialog.Layer.vue';
|
|||
import * as os from '@/os.js';
|
||||
import { deepClone } from '@/utility/clone.js';
|
||||
import { ensureSignin } from '@/i.js';
|
||||
import { FX_watermarkPlacement } from '@/utility/image-effector/fxs/watermarkPlacement.js';
|
||||
|
||||
const $i = ensureSignin();
|
||||
|
||||
|
@ -122,7 +119,7 @@ watch(type, () => {
|
|||
|
||||
watch(preset, async (newValue, oldValue) => {
|
||||
if (renderer != null) {
|
||||
renderer.updateLayers(makeImageEffectorLayers(preset.layers));
|
||||
renderer.setLayers(preset.layers);
|
||||
}
|
||||
}, { deep: true });
|
||||
|
||||
|
@ -149,32 +146,28 @@ watch(sampleImageType, async () => {
|
|||
}
|
||||
});
|
||||
|
||||
let renderer: ImageEffector | null = null;
|
||||
let renderer: WatermarkRenderer | null = null;
|
||||
|
||||
async function initRenderer() {
|
||||
if (canvasEl.value == null) return;
|
||||
|
||||
if (sampleImageType.value === '3_2') {
|
||||
renderer = new ImageEffector({
|
||||
renderer = new WatermarkRenderer({
|
||||
canvas: canvasEl.value,
|
||||
width: 1500,
|
||||
height: 1000,
|
||||
layers: makeImageEffectorLayers(preset.layers),
|
||||
originalImage: sampleImage_3_2,
|
||||
fxs: [FX_watermarkPlacement],
|
||||
renderWidth: 1500,
|
||||
renderHeight: 1000,
|
||||
image: sampleImage_3_2,
|
||||
});
|
||||
} else if (sampleImageType.value === '2_3') {
|
||||
renderer = new ImageEffector({
|
||||
renderer = new WatermarkRenderer({
|
||||
canvas: canvasEl.value,
|
||||
width: 1000,
|
||||
height: 1500,
|
||||
layers: makeImageEffectorLayers(preset.layers),
|
||||
originalImage: sampleImage_2_3,
|
||||
fxs: [FX_watermarkPlacement],
|
||||
renderWidth: 1000,
|
||||
renderHeight: 1500,
|
||||
image: sampleImage_2_3,
|
||||
});
|
||||
}
|
||||
|
||||
await renderer!.bakeTextures();
|
||||
await renderer!.setLayers(preset.layers);
|
||||
|
||||
renderer!.render();
|
||||
}
|
||||
|
|
|
@ -23,14 +23,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue';
|
||||
import type { WatermarkPreset } from '@/utility/watermark.js';
|
||||
import { makeImageEffectorLayers } from '@/utility/watermark.js';
|
||||
import { WatermarkRenderer } from '@/utility/watermark.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { deepClone } from '@/utility/clone.js';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import { ImageEffector } from '@/utility/image-effector/ImageEffector.js';
|
||||
import { FX_watermarkPlacement } from '@/utility/image-effector/fxs/watermarkPlacement.js';
|
||||
|
||||
const props = defineProps<{
|
||||
preset: WatermarkPreset;
|
||||
|
@ -66,23 +64,21 @@ const canvasEl = useTemplateRef('canvasEl');
|
|||
const sampleImage = new Image();
|
||||
sampleImage.src = '/client-assets/sample/3-2.jpg';
|
||||
|
||||
let renderer: ImageEffector | null = null;
|
||||
let renderer: WatermarkRenderer | null = null;
|
||||
|
||||
onMounted(() => {
|
||||
sampleImage.onload = async () => {
|
||||
watch(canvasEl, async () => {
|
||||
if (canvasEl.value == null) return;
|
||||
|
||||
renderer = new ImageEffector({
|
||||
renderer = new WatermarkRenderer({
|
||||
canvas: canvasEl.value,
|
||||
width: 1500,
|
||||
height: 1000,
|
||||
layers: makeImageEffectorLayers(props.preset.layers),
|
||||
originalImage: sampleImage,
|
||||
fxs: [FX_watermarkPlacement],
|
||||
renderWidth: 1500,
|
||||
renderHeight: 1000,
|
||||
image: sampleImage,
|
||||
});
|
||||
|
||||
await renderer.bakeTextures();
|
||||
await renderer.setLayers(props.preset.layers);
|
||||
|
||||
renderer.render();
|
||||
}, { immediate: true });
|
||||
|
@ -98,8 +94,7 @@ onUnmounted(() => {
|
|||
|
||||
watch(() => props.preset, async () => {
|
||||
if (renderer != null) {
|
||||
renderer.updateLayers(makeImageEffectorLayers(props.preset.layers));
|
||||
await renderer.bakeTextures();
|
||||
await renderer.setLayers(props.preset.layers);
|
||||
renderer.render();
|
||||
}
|
||||
}, { deep: true });
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { getProxiedImageUrl } from '../media-proxy.js';
|
||||
import { createTexture } from './utilts.js';
|
||||
|
||||
type ParamTypeToPrimitive = {
|
||||
'number': number;
|
||||
|
@ -11,6 +11,7 @@ type ParamTypeToPrimitive = {
|
|||
'boolean': boolean;
|
||||
'align': { x: 'left' | 'center' | 'right'; y: 'top' | 'center' | 'bottom'; };
|
||||
'seed': number;
|
||||
'texture': string | null;
|
||||
};
|
||||
|
||||
type ImageEffectorFxParamDefs = Record<string, {
|
||||
|
@ -22,26 +23,27 @@ export function defineImageEffectorFx<ID extends string, P extends ImageEffector
|
|||
return fx;
|
||||
}
|
||||
|
||||
export type ImageEffectorFx<ID extends string = string, P extends ImageEffectorFxParamDefs = any, U extends string[] = string[]> = {
|
||||
export type ImageEffectorFx<ID extends string = string, PS extends ImageEffectorFxParamDefs = any, US extends string[] = string[], TS extends string[] = string[]> = {
|
||||
id: ID;
|
||||
name: string;
|
||||
shader: string;
|
||||
uniforms: U;
|
||||
params: P,
|
||||
uniforms: US;
|
||||
params: PS,
|
||||
textures?: TS;
|
||||
main: (ctx: {
|
||||
gl: WebGL2RenderingContext;
|
||||
program: WebGLProgram;
|
||||
params: {
|
||||
[key in keyof P]: ParamTypeToPrimitive[P[key]['type']];
|
||||
[key in keyof PS]: ParamTypeToPrimitive[PS[key]['type']];
|
||||
};
|
||||
u: Record<U[number], WebGLUniformLocation>;
|
||||
u: Record<US[number], WebGLUniformLocation>;
|
||||
width: number;
|
||||
height: number;
|
||||
watermark?: {
|
||||
textures: Record<TS[number], {
|
||||
texture: WebGLTexture;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
} | null>;
|
||||
}) => void;
|
||||
};
|
||||
|
||||
|
@ -49,54 +51,54 @@ export type ImageEffectorLayer = {
|
|||
id: string;
|
||||
fxId: string;
|
||||
params: Record<string, any>;
|
||||
|
||||
// for watermarkPlacement fx
|
||||
imageUrl?: string | null;
|
||||
text?: string | null;
|
||||
textures?: Record<string, ExternalTextureId | null>;
|
||||
};
|
||||
|
||||
type ExternalTextureId = string;
|
||||
|
||||
export class ImageEffector {
|
||||
public gl: WebGL2RenderingContext;
|
||||
private canvas: HTMLCanvasElement | null = null;
|
||||
private gl: WebGL2RenderingContext | null = null;
|
||||
private renderTextureProgram!: WebGLProgram;
|
||||
private renderInvertedTextureProgram!: WebGLProgram;
|
||||
private renderWidth!: number;
|
||||
private renderHeight!: number;
|
||||
private originalImage: ImageData | ImageBitmap | HTMLImageElement | HTMLCanvasElement;
|
||||
private layers: ImageEffectorLayer[];
|
||||
private layers: ImageEffectorLayer[] = [];
|
||||
private originalImageTexture: WebGLTexture;
|
||||
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();
|
||||
private fxs: ImageEffectorFx[];
|
||||
private externalTextures: Map<ExternalTextureId, { texture: WebGLTexture; width: number; height: number; }> = new Map();
|
||||
|
||||
constructor(options: {
|
||||
canvas: HTMLCanvasElement;
|
||||
width: number;
|
||||
height: number;
|
||||
originalImage: ImageData | ImageBitmap | HTMLImageElement | HTMLCanvasElement;
|
||||
layers: ImageEffectorLayer[];
|
||||
renderWidth: number;
|
||||
renderHeight: number;
|
||||
image: ImageData | ImageBitmap | HTMLImageElement | HTMLCanvasElement;
|
||||
fxs: ImageEffectorFx[];
|
||||
}) {
|
||||
this.canvas = options.canvas;
|
||||
this.canvas.width = options.width;
|
||||
this.canvas.height = options.height;
|
||||
this.renderWidth = options.width;
|
||||
this.renderHeight = options.height;
|
||||
this.originalImage = options.originalImage;
|
||||
this.layers = options.layers;
|
||||
this.renderWidth = options.renderWidth;
|
||||
this.renderHeight = options.renderHeight;
|
||||
this.originalImage = options.image;
|
||||
this.fxs = options.fxs;
|
||||
this.texturesKey = this.calcTexturesKey();
|
||||
|
||||
this.gl = this.canvas.getContext('webgl2', {
|
||||
this.canvas.width = this.renderWidth;
|
||||
this.canvas.height = this.renderHeight;
|
||||
|
||||
const gl = this.canvas.getContext('webgl2', {
|
||||
preserveDrawingBuffer: false,
|
||||
alpha: true,
|
||||
premultipliedAlpha: false,
|
||||
})!;
|
||||
});
|
||||
|
||||
const gl = this.gl;
|
||||
if (gl == null) {
|
||||
throw new Error('Failed to initialize WebGL2 context');
|
||||
}
|
||||
|
||||
this.gl = gl;
|
||||
|
||||
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
|
||||
|
||||
|
@ -105,10 +107,10 @@ export class ImageEffector {
|
|||
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, VERTICES, gl.STATIC_DRAW);
|
||||
|
||||
this.originalImageTexture = this.createTexture();
|
||||
this.originalImageTexture = createTexture(gl);
|
||||
gl.activeTexture(gl.TEXTURE0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.originalImageTexture);
|
||||
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, this.originalImage.width, this.originalImage.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, this.originalImage);
|
||||
gl.bindTexture(gl.TEXTURE_2D, null);
|
||||
|
||||
this.renderTextureProgram = this.initShaderProgram(`#version 300 es
|
||||
|
@ -153,119 +155,8 @@ export class ImageEffector {
|
|||
`)!;
|
||||
}
|
||||
|
||||
private calcTexturesKey() {
|
||||
return this.layers.map(layer => {
|
||||
if (layer.fxId === 'watermarkPlacement' && layer.imageUrl != null) {
|
||||
return layer.imageUrl;
|
||||
} else if (layer.fxId === 'watermarkPlacement' && layer.text != null) {
|
||||
return layer.text;
|
||||
}
|
||||
return '';
|
||||
}).join(';');
|
||||
}
|
||||
|
||||
private createTexture(): WebGLTexture {
|
||||
const gl = this.gl!;
|
||||
const texture = gl.createTexture();
|
||||
gl.bindTexture(gl.TEXTURE_2D, texture);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
||||
gl.bindTexture(gl.TEXTURE_2D, null);
|
||||
return texture!;
|
||||
}
|
||||
|
||||
public disposeBakedTextures() {
|
||||
const gl = this.gl;
|
||||
if (gl == null) {
|
||||
throw new Error('gl is not initialized');
|
||||
}
|
||||
|
||||
for (const bakedTexture of this.bakedTexturesForWatermarkFx.values()) {
|
||||
gl.deleteTexture(bakedTexture.texture);
|
||||
}
|
||||
this.bakedTexturesForWatermarkFx.clear();
|
||||
}
|
||||
|
||||
public async bakeTextures() {
|
||||
const gl = this.gl;
|
||||
if (gl == null) {
|
||||
throw new Error('gl is not initialized');
|
||||
}
|
||||
|
||||
console.log('baking textures', this.texturesKey);
|
||||
|
||||
this.disposeBakedTextures();
|
||||
|
||||
for (const layer of this.layers) {
|
||||
if (layer.fxId === 'watermarkPlacement' && layer.imageUrl != null) {
|
||||
const image = await new Promise<HTMLImageElement>((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.onload = () => resolve(img);
|
||||
img.onerror = reject;
|
||||
img.src = getProxiedImageUrl(layer.imageUrl); // CORS対策
|
||||
});
|
||||
|
||||
const texture = this.createTexture();
|
||||
gl.activeTexture(gl.TEXTURE0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, texture);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, image.width, image.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, image);
|
||||
gl.bindTexture(gl.TEXTURE_2D, null);
|
||||
|
||||
this.bakedTexturesForWatermarkFx.set(layer.id, {
|
||||
texture: texture,
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
});
|
||||
} else if (layer.fxId === 'watermarkPlacement' && layer.text != null) {
|
||||
const measureCtx = window.document.createElement('canvas').getContext('2d')!;
|
||||
measureCtx.canvas.width = this.renderWidth;
|
||||
measureCtx.canvas.height = this.renderHeight;
|
||||
const fontSize = Math.min(this.renderWidth, this.renderHeight) / 20;
|
||||
const margin = Math.min(this.renderWidth, this.renderHeight) / 50;
|
||||
measureCtx.font = `bold ${fontSize}px sans-serif`;
|
||||
const textMetrics = measureCtx.measureText(layer.text);
|
||||
measureCtx.canvas.remove();
|
||||
|
||||
const RESOLUTION_FACTOR = 4;
|
||||
|
||||
const textCtx = window.document.createElement('canvas').getContext('2d')!;
|
||||
textCtx.canvas.width = (Math.ceil(textMetrics.actualBoundingBoxRight + textMetrics.actualBoundingBoxLeft) + margin + margin) * RESOLUTION_FACTOR;
|
||||
textCtx.canvas.height = (Math.ceil(textMetrics.actualBoundingBoxAscent + textMetrics.actualBoundingBoxDescent) + margin + margin) * RESOLUTION_FACTOR;
|
||||
|
||||
//textCtx.fillStyle = '#00ff00';
|
||||
//textCtx.fillRect(0, 0, textCtx.canvas.width, textCtx.canvas.height);
|
||||
|
||||
textCtx.shadowColor = '#000000';
|
||||
textCtx.shadowBlur = 10 * RESOLUTION_FACTOR;
|
||||
|
||||
textCtx.fillStyle = '#ffffff';
|
||||
textCtx.font = `bold ${fontSize * RESOLUTION_FACTOR}px sans-serif`;
|
||||
textCtx.textBaseline = 'middle';
|
||||
textCtx.textAlign = 'center';
|
||||
|
||||
textCtx.fillText(layer.text, textCtx.canvas.width / 2, textCtx.canvas.height / 2);
|
||||
|
||||
const texture = this.createTexture();
|
||||
gl.activeTexture(gl.TEXTURE0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, texture);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, textCtx.canvas.width, textCtx.canvas.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, textCtx.canvas);
|
||||
gl.bindTexture(gl.TEXTURE_2D, null);
|
||||
|
||||
this.bakedTexturesForWatermarkFx.set(layer.id, {
|
||||
texture: texture,
|
||||
width: textCtx.canvas.width,
|
||||
height: textCtx.canvas.height,
|
||||
});
|
||||
|
||||
textCtx.canvas.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public loadShader(type, source) {
|
||||
const gl = this.gl!;
|
||||
const gl = this.gl;
|
||||
|
||||
const shader = gl.createShader(type)!;
|
||||
|
||||
|
@ -284,7 +175,7 @@ export class ImageEffector {
|
|||
}
|
||||
|
||||
public initShaderProgram(vsSource, fsSource): WebGLProgram {
|
||||
const gl = this.gl!;
|
||||
const gl = this.gl;
|
||||
|
||||
const vertexShader = this.loadShader(gl.VERTEX_SHADER, vsSource)!;
|
||||
const fragmentShader = this.loadShader(gl.FRAGMENT_SHADER, fsSource)!;
|
||||
|
@ -308,15 +199,10 @@ export class ImageEffector {
|
|||
|
||||
private renderLayer(layer: ImageEffectorLayer, preTexture: WebGLTexture) {
|
||||
const gl = this.gl;
|
||||
if (gl == null) {
|
||||
throw new Error('gl is not initialized');
|
||||
}
|
||||
|
||||
const fx = this.fxs.find(fx => fx.id === layer.fxId);
|
||||
if (fx == null) return;
|
||||
|
||||
const watermark = layer.fxId === 'watermarkPlacement' ? this.bakedTexturesForWatermarkFx.get(layer.id) : undefined;
|
||||
|
||||
const cachedShader = this.shaderCache.get(fx.id);
|
||||
const shaderProgram = cachedShader ?? this.initShaderProgram(`#version 300 es
|
||||
in vec2 position;
|
||||
|
@ -352,7 +238,13 @@ export class ImageEffector {
|
|||
u: Object.fromEntries(fx.uniforms.map(u => [u, gl.getUniformLocation(shaderProgram, 'u_' + u)!])),
|
||||
width: this.renderWidth,
|
||||
height: this.renderHeight,
|
||||
watermark: watermark,
|
||||
textures: Object.fromEntries(
|
||||
Object.entries(layer.textures ?? {}).map(([key, textureId]) => {
|
||||
if (textureId == null) return [key, null];
|
||||
const externalTexture = this.externalTextures.get(textureId);
|
||||
if (externalTexture == null) return [key, null];
|
||||
return [key, externalTexture];
|
||||
})),
|
||||
});
|
||||
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||||
|
@ -360,9 +252,6 @@ export class ImageEffector {
|
|||
|
||||
public render() {
|
||||
const gl = this.gl;
|
||||
if (gl == null) {
|
||||
throw new Error('gl is not initialized');
|
||||
}
|
||||
|
||||
{
|
||||
gl.activeTexture(gl.TEXTURE0);
|
||||
|
@ -386,7 +275,7 @@ export class ImageEffector {
|
|||
|
||||
for (const layer of this.layers) {
|
||||
const cachedResultTexture = this.perLayerResultTextures.get(layer.id);
|
||||
const resultTexture = cachedResultTexture ?? this.createTexture();
|
||||
const resultTexture = cachedResultTexture ?? createTexture(gl);
|
||||
if (cachedResultTexture == null) {
|
||||
this.perLayerResultTextures.set(layer.id, resultTexture);
|
||||
}
|
||||
|
@ -420,42 +309,41 @@ export class ImageEffector {
|
|||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||||
}
|
||||
|
||||
public async updateLayers(layers: ImageEffectorLayer[]) {
|
||||
public async setLayers(layers: ImageEffectorLayer[]) {
|
||||
this.layers = layers;
|
||||
|
||||
const newTexturesKey = this.calcTexturesKey();
|
||||
if (newTexturesKey !== this.texturesKey) {
|
||||
this.texturesKey = newTexturesKey;
|
||||
await this.bakeTextures();
|
||||
}
|
||||
|
||||
this.render();
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
const gl = this.gl;
|
||||
if (gl == null) {
|
||||
throw new Error('gl is not initialized');
|
||||
public registerExternalTexture(id: string, texture: WebGLTexture, width: number, height: number) {
|
||||
this.externalTextures.set(id, { texture, width, height });
|
||||
}
|
||||
|
||||
public disposeExternalTextures() {
|
||||
for (const bakedTexture of this.externalTextures.values()) {
|
||||
this.gl.deleteTexture(bakedTexture.texture);
|
||||
}
|
||||
this.externalTextures.clear();
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
for (const shader of this.shaderCache.values()) {
|
||||
gl.deleteProgram(shader);
|
||||
this.gl.deleteProgram(shader);
|
||||
}
|
||||
this.shaderCache.clear();
|
||||
|
||||
for (const texture of this.perLayerResultTextures.values()) {
|
||||
gl.deleteTexture(texture);
|
||||
this.gl.deleteTexture(texture);
|
||||
}
|
||||
this.perLayerResultTextures.clear();
|
||||
|
||||
for (const framebuffer of this.perLayerResultFrameBuffers.values()) {
|
||||
gl.deleteFramebuffer(framebuffer);
|
||||
this.gl.deleteFramebuffer(framebuffer);
|
||||
}
|
||||
this.perLayerResultFrameBuffers.clear();
|
||||
|
||||
this.disposeBakedTextures();
|
||||
gl.deleteProgram(this.renderTextureProgram);
|
||||
gl.deleteProgram(this.renderInvertedTextureProgram);
|
||||
gl.deleteTexture(this.originalImageTexture);
|
||||
this.disposeExternalTextures();
|
||||
this.gl.deleteProgram(this.renderTextureProgram);
|
||||
this.gl.deleteProgram(this.renderInvertedTextureProgram);
|
||||
this.gl.deleteTexture(this.originalImageTexture);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -99,16 +99,17 @@ export const FX_watermarkPlacement = defineImageEffectorFx({
|
|||
step: 0.01,
|
||||
},
|
||||
},
|
||||
main: ({ gl, u, params, watermark }) => {
|
||||
if (watermark == null) {
|
||||
textures: ['watermark'] as const,
|
||||
main: ({ gl, u, params, textures }) => {
|
||||
if (textures.watermark == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
gl.activeTexture(gl.TEXTURE1);
|
||||
gl.bindTexture(gl.TEXTURE_2D, watermark.texture);
|
||||
gl.bindTexture(gl.TEXTURE_2D, textures.watermark.texture);
|
||||
gl.uniform1i(u.texture_watermark, 1);
|
||||
|
||||
gl.uniform2fv(u.resolution_watermark, [watermark.width, watermark.height]);
|
||||
gl.uniform2fv(u.resolution_watermark, [textures.watermark.width, textures.watermark.height]);
|
||||
gl.uniform1f(u.scale, params.scale);
|
||||
gl.uniform1f(u.opacity, params.opacity);
|
||||
gl.uniform1f(u.angle, 0.0);
|
||||
|
|
|
@ -69,7 +69,7 @@ export const FX_zoomLines = defineImageEffectorFx({
|
|||
},
|
||||
threshold: {
|
||||
type: 'number' as const,
|
||||
default: 0.5,
|
||||
default: 0.8,
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
step: 0.01,
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { getProxiedImageUrl } from '../media-proxy.js';
|
||||
|
||||
export function createTexture(gl: WebGL2RenderingContext): WebGLTexture {
|
||||
const texture = gl.createTexture();
|
||||
gl.bindTexture(gl.TEXTURE_2D, texture);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
||||
gl.bindTexture(gl.TEXTURE_2D, null);
|
||||
return texture;
|
||||
}
|
||||
|
||||
export async function createTextureFromUrl(gl: WebGL2RenderingContext, imageUrl: string): Promise<{ texture: WebGLTexture, width: number, height: number }> {
|
||||
const image = await new Promise<HTMLImageElement>((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.onload = () => resolve(img);
|
||||
img.onerror = reject;
|
||||
img.src = getProxiedImageUrl(imageUrl); // CORS対策
|
||||
});
|
||||
|
||||
const texture = createTexture(gl);
|
||||
gl.activeTexture(gl.TEXTURE0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, texture);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, image.width, image.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, image);
|
||||
gl.bindTexture(gl.TEXTURE_2D, null);
|
||||
|
||||
return {
|
||||
texture,
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
};
|
||||
}
|
||||
|
||||
export async function createTextureFromText(gl: WebGL2RenderingContext, text: string, resolution = 2048): Promise<{ texture: WebGLTexture, width: number, height: number }> {
|
||||
const ctx = window.document.createElement('canvas').getContext('2d')!;
|
||||
ctx.canvas.width = resolution;
|
||||
ctx.canvas.height = resolution / 4;
|
||||
const fontSize = resolution / 32;
|
||||
const margin = fontSize / 2;
|
||||
ctx.shadowColor = '#000000';
|
||||
ctx.shadowBlur = fontSize / 4;
|
||||
|
||||
//ctx.fillStyle = '#00ff00';
|
||||
//ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.font = `bold ${fontSize}px sans-serif`;
|
||||
ctx.textBaseline = 'middle';
|
||||
|
||||
ctx.fillText(text, margin, ctx.canvas.height / 2);
|
||||
|
||||
const textMetrics = ctx.measureText(text);
|
||||
const cropWidth = (Math.ceil(textMetrics.actualBoundingBoxRight + textMetrics.actualBoundingBoxLeft) + margin + margin);
|
||||
const cropHeight = (Math.ceil(textMetrics.actualBoundingBoxAscent + textMetrics.actualBoundingBoxDescent) + margin + margin);
|
||||
const data = ctx.getImageData(0, (ctx.canvas.height / 2) - (cropHeight / 2), ctx.canvas.width, ctx.canvas.height);
|
||||
|
||||
const texture = createTexture(gl);
|
||||
gl.activeTexture(gl.TEXTURE0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, texture);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, cropWidth, cropHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);
|
||||
gl.bindTexture(gl.TEXTURE_2D, null);
|
||||
|
||||
const info = {
|
||||
texture: texture,
|
||||
width: cropWidth,
|
||||
height: cropHeight,
|
||||
};
|
||||
|
||||
ctx.canvas.remove();
|
||||
|
||||
return info;
|
||||
}
|
|
@ -3,7 +3,10 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { FX_watermarkPlacement } from './image-effector/fxs/watermarkPlacement.js';
|
||||
import { createTextureFromText, createTextureFromUrl } from './image-effector/utilts.js';
|
||||
import type { ImageEffectorLayer } from '@/utility/image-effector/ImageEffector.js';
|
||||
import { ImageEffector } from '@/utility/image-effector/ImageEffector.js';
|
||||
|
||||
export type WatermarkPreset = {
|
||||
id: string;
|
||||
|
@ -29,8 +32,52 @@ export type WatermarkPreset = {
|
|||
})[];
|
||||
};
|
||||
|
||||
export function makeImageEffectorLayers(layers: WatermarkPreset['layers']): ImageEffectorLayer[] {
|
||||
return layers.map(layer => {
|
||||
export class WatermarkRenderer {
|
||||
private effector: ImageEffector;
|
||||
private layers: WatermarkPreset['layers'] = [];
|
||||
private texturesKey = '';
|
||||
|
||||
constructor(options: {
|
||||
canvas: HTMLCanvasElement,
|
||||
renderWidth: number,
|
||||
renderHeight: number,
|
||||
image: HTMLImageElement,
|
||||
}) {
|
||||
this.effector = new ImageEffector({
|
||||
canvas: options.canvas,
|
||||
renderWidth: options.renderWidth,
|
||||
renderHeight: options.renderHeight,
|
||||
image: options.image,
|
||||
fxs: [FX_watermarkPlacement],
|
||||
});
|
||||
}
|
||||
|
||||
private calcTexturesKey() {
|
||||
return this.layers.map(layer => {
|
||||
if (layer.type === 'image' && layer.imageUrl != null) {
|
||||
return layer.imageUrl;
|
||||
} else if (layer.type === 'text' && layer.text != null) {
|
||||
return layer.text;
|
||||
}
|
||||
return '';
|
||||
}).join(';');
|
||||
}
|
||||
|
||||
private async bakeTextures(): Promise<void> {
|
||||
this.effector.disposeExternalTextures();
|
||||
for (const layer of this.layers) {
|
||||
if (layer.type === 'text' && layer.text != null) {
|
||||
const { texture, width, height } = await createTextureFromText(this.effector.gl, layer.text);
|
||||
this.effector.registerExternalTexture(layer.id, texture, width, height);
|
||||
} else if (layer.type === 'image' && layer.imageUrl != null) {
|
||||
const { texture, width, height } = await createTextureFromUrl(this.effector.gl, layer.imageUrl);
|
||||
this.effector.registerExternalTexture(layer.id, texture, width, height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private makeImageEffectorLayers(): ImageEffectorLayer[] {
|
||||
return this.layers.map(layer => {
|
||||
if (layer.type === 'text') {
|
||||
return {
|
||||
fxId: 'watermarkPlacement',
|
||||
|
@ -42,8 +89,7 @@ export function makeImageEffectorLayers(layers: WatermarkPreset['layers']): Imag
|
|||
opacity: layer.opacity,
|
||||
cover: false,
|
||||
},
|
||||
text: layer.text,
|
||||
imageUrl: null,
|
||||
textures: { watermark: layer.id },
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
|
@ -56,9 +102,31 @@ export function makeImageEffectorLayers(layers: WatermarkPreset['layers']): Imag
|
|||
opacity: layer.opacity,
|
||||
cover: layer.cover,
|
||||
},
|
||||
text: null,
|
||||
imageUrl: layer.imageUrl ?? null,
|
||||
textures: { watermark: layer.id },
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async setLayers(layers: WatermarkPreset['layers']) {
|
||||
this.layers = layers;
|
||||
|
||||
const newTexturesKey = this.calcTexturesKey();
|
||||
if (newTexturesKey !== this.texturesKey) {
|
||||
this.texturesKey = newTexturesKey;
|
||||
await this.bakeTextures();
|
||||
}
|
||||
|
||||
this.effector.setLayers(this.makeImageEffectorLayers());
|
||||
|
||||
this.render();
|
||||
}
|
||||
|
||||
public render(): void {
|
||||
this.effector.render();
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
this.effector.destroy();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue