wip
This commit is contained in:
		
							parent
							
								
									1e9faff0db
								
							
						
					
					
						commit
						0e37048e1e
					
				|  | @ -3,7 +3,7 @@ | |||
|  * SPDX-License-Identifier: AGPL-3.0-only | ||||
|  */ | ||||
| 
 | ||||
| import { createTexture } from './utilts.js'; | ||||
| import { getProxiedImageUrl } from '../media-proxy.js'; | ||||
| 
 | ||||
| type ParamTypeToPrimitive = { | ||||
| 	'number': number; | ||||
|  | @ -11,7 +11,7 @@ type ParamTypeToPrimitive = { | |||
| 	'boolean': boolean; | ||||
| 	'align': { x: 'left' | 'center' | 'right'; y: 'top' | 'center' | 'bottom'; }; | ||||
| 	'seed': number; | ||||
| 	'texture': string | null; | ||||
| 	'texture': { type: 'text'; text: string | null; } | { type: 'url'; url: string | null; } | null; | ||||
| }; | ||||
| 
 | ||||
| type ImageEffectorFxParamDefs = Record<string, { | ||||
|  | @ -19,17 +19,16 @@ type ImageEffectorFxParamDefs = Record<string, { | |||
| 	default: any; | ||||
| }>; | ||||
| 
 | ||||
| export function defineImageEffectorFx<ID extends string, PS extends ImageEffectorFxParamDefs, US extends string[], TS extends string[]>(fx: ImageEffectorFx<ID, PS, US, TS>) { | ||||
| export function defineImageEffectorFx<ID extends string, PS extends ImageEffectorFxParamDefs, US extends string[]>(fx: ImageEffectorFx<ID, PS, US>) { | ||||
| 	return fx; | ||||
| } | ||||
| 
 | ||||
| export type ImageEffectorFx<ID extends string = string, PS extends ImageEffectorFxParamDefs = any, US extends string[] = string[], TS extends string[] = string[]> = { | ||||
| export type ImageEffectorFx<ID extends string = string, PS extends ImageEffectorFxParamDefs = ImageEffectorFxParamDefs, US extends string[] = string[]> = { | ||||
| 	id: ID; | ||||
| 	name: string; | ||||
| 	shader: string; | ||||
| 	uniforms: US; | ||||
| 	params: PS, | ||||
| 	textures?: TS; | ||||
| 	main: (ctx: { | ||||
| 		gl: WebGL2RenderingContext; | ||||
| 		program: WebGLProgram; | ||||
|  | @ -39,7 +38,7 @@ export type ImageEffectorFx<ID extends string = string, PS extends ImageEffector | |||
| 		u: Record<US[number], WebGLUniformLocation>; | ||||
| 		width: number; | ||||
| 		height: number; | ||||
| 		textures: Record<TS[number], { | ||||
| 		textures: Record<string, { | ||||
| 			texture: WebGLTexture; | ||||
| 			width: number; | ||||
| 			height: number; | ||||
|  | @ -51,13 +50,10 @@ export type ImageEffectorLayer = { | |||
| 	id: string; | ||||
| 	fxId: string; | ||||
| 	params: Record<string, any>; | ||||
| 	textures?: Record<string, ExternalTextureId | null>; | ||||
| }; | ||||
| 
 | ||||
| type ExternalTextureId = string; | ||||
| 
 | ||||
| export class ImageEffector { | ||||
| 	public gl: WebGL2RenderingContext; | ||||
| 	private gl: WebGL2RenderingContext; | ||||
| 	private canvas: HTMLCanvasElement | null = null; | ||||
| 	private renderTextureProgram!: WebGLProgram; | ||||
| 	private renderInvertedTextureProgram!: WebGLProgram; | ||||
|  | @ -70,7 +66,7 @@ export class ImageEffector { | |||
| 	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(); | ||||
| 	private paramTextures: Map<string, { texture: WebGLTexture; width: number; height: number; }> = new Map(); | ||||
| 
 | ||||
| 	constructor(options: { | ||||
| 		canvas: HTMLCanvasElement; | ||||
|  | @ -239,11 +235,12 @@ export class ImageEffector { | |||
| 			width: this.renderWidth, | ||||
| 			height: this.renderHeight, | ||||
| 			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]; | ||||
| 				Object.entries(fx.params).map(([k, v]) => { | ||||
| 					if (v.type !== 'texture') return [k, null]; | ||||
| 					const param = layer.params[k]; | ||||
| 					if (param == null) return [k, null]; | ||||
| 					const texture = this.paramTextures.get(this.getTextureKeyForParam(param)); | ||||
| 					return [k, texture]; | ||||
| 				})), | ||||
| 		}); | ||||
| 
 | ||||
|  | @ -311,18 +308,44 @@ export class ImageEffector { | |||
| 
 | ||||
| 	public async setLayers(layers: ImageEffectorLayer[]) { | ||||
| 		this.layers = layers; | ||||
| 
 | ||||
| 		const unused = new Set(this.paramTextures.keys()); | ||||
| 
 | ||||
| 		for (const layer of layers) { | ||||
| 			const fx = this.fxs.find(fx => fx.id === layer.fxId); | ||||
| 			if (fx == null) continue; | ||||
| 
 | ||||
| 			for (const [k, v] of Object.entries(layer.params)) { | ||||
| 				const paramDef = fx.params[k]; | ||||
| 				if (paramDef == null) continue; | ||||
| 				if (paramDef.type !== 'texture') continue; | ||||
| 				if (v == null) continue; | ||||
| 
 | ||||
| 				const textureKey = this.getTextureKeyForParam(v); | ||||
| 				unused.delete(textureKey); | ||||
| 				if (this.paramTextures.has(textureKey)) continue; | ||||
| 
 | ||||
| 				console.log(`Baking texture of <${textureKey}>...`); | ||||
| 
 | ||||
| 				const texture = v.type === 'text' ? await createTextureFromText(this.gl, v.text) : v.type === 'url' ? await createTextureFromUrl(this.gl, v.url) : null; | ||||
| 				if (texture == null) continue; | ||||
| 
 | ||||
| 				this.paramTextures.set(textureKey, texture); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		for (const k of unused) { | ||||
| 			console.log(`Dispose unused texture <${k}>...`); | ||||
| 			this.gl.deleteTexture(this.paramTextures.get(k)!.texture); | ||||
| 			this.paramTextures.delete(k); | ||||
| 		} | ||||
| 
 | ||||
| 		this.render(); | ||||
| 	} | ||||
| 
 | ||||
| 	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(); | ||||
| 	private getTextureKeyForParam(v: ParamTypeToPrimitive['texture']) { | ||||
| 		if (v == null) return ''; | ||||
| 		return v.type === 'text' ? `text:${v.text}` : v.type === 'url' ? `url:${v.url}` : ''; | ||||
| 	} | ||||
| 
 | ||||
| 	public destroy() { | ||||
|  | @ -341,9 +364,89 @@ export class ImageEffector { | |||
| 		} | ||||
| 		this.perLayerResultFrameBuffers.clear(); | ||||
| 
 | ||||
| 		this.disposeExternalTextures(); | ||||
| 		for (const texture of this.paramTextures.values()) { | ||||
| 			this.gl.deleteTexture(texture.texture); | ||||
| 		} | ||||
| 		this.paramTextures.clear(); | ||||
| 
 | ||||
| 		this.gl.deleteProgram(this.renderTextureProgram); | ||||
| 		this.gl.deleteProgram(this.renderInvertedTextureProgram); | ||||
| 		this.gl.deleteTexture(this.originalImageTexture); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| 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; | ||||
| } | ||||
| 
 | ||||
| async function createTextureFromUrl(gl: WebGL2RenderingContext, imageUrl: string | null): Promise<{ texture: WebGLTexture, width: number, height: number } | null> { | ||||
| 	if (imageUrl == null || imageUrl.trim() === '') return null; | ||||
| 
 | ||||
| 	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, | ||||
| 	}; | ||||
| } | ||||
| 
 | ||||
| async function createTextureFromText(gl: WebGL2RenderingContext, text: string | null, resolution = 2048): Promise<{ texture: WebGLTexture, width: number, height: number } | null> { | ||||
| 	if (text == null || text.trim() === '') return null; | ||||
| 
 | ||||
| 	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; | ||||
| } | ||||
|  |  | |||
|  | @ -98,8 +98,11 @@ export const FX_watermarkPlacement = defineImageEffectorFx({ | |||
| 			max: 1.0, | ||||
| 			step: 0.01, | ||||
| 		}, | ||||
| 		watermark: { | ||||
| 			type: 'texture' as const, | ||||
| 			default: null, | ||||
| 		}, | ||||
| 	}, | ||||
| 	textures: ['watermark'] as const, | ||||
| 	main: ({ gl, u, params, textures }) => { | ||||
| 		if (textures.watermark == null) { | ||||
| 			return; | ||||
|  |  | |||
|  | @ -1,78 +0,0 @@ | |||
| /* | ||||
|  * 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; | ||||
| } | ||||
|  | @ -4,7 +4,6 @@ | |||
|  */ | ||||
| 
 | ||||
| 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'; | ||||
| 
 | ||||
|  | @ -35,7 +34,6 @@ export type WatermarkPreset = { | |||
| export class WatermarkRenderer { | ||||
| 	private effector: ImageEffector; | ||||
| 	private layers: WatermarkPreset['layers'] = []; | ||||
| 	private texturesKey = ''; | ||||
| 
 | ||||
| 	constructor(options: { | ||||
| 		canvas: HTMLCanvasElement, | ||||
|  | @ -52,30 +50,6 @@ export class WatermarkRenderer { | |||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	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') { | ||||
|  | @ -88,8 +62,11 @@ export class WatermarkRenderer { | |||
| 						align: layer.align, | ||||
| 						opacity: layer.opacity, | ||||
| 						cover: false, | ||||
| 						watermark: { | ||||
| 							type: 'text', | ||||
| 							text: layer.text, | ||||
| 						}, | ||||
| 					}, | ||||
| 					textures: { watermark: layer.id }, | ||||
| 				}; | ||||
| 			} else { | ||||
| 				return { | ||||
|  | @ -101,8 +78,11 @@ export class WatermarkRenderer { | |||
| 						align: layer.align, | ||||
| 						opacity: layer.opacity, | ||||
| 						cover: layer.cover, | ||||
| 						watermark: { | ||||
| 							type: 'url', | ||||
| 							url: layer.imageUrl, | ||||
| 						}, | ||||
| 					}, | ||||
| 					textures: { watermark: layer.id }, | ||||
| 				}; | ||||
| 			} | ||||
| 		}); | ||||
|  | @ -110,15 +90,7 @@ export class WatermarkRenderer { | |||
| 
 | ||||
| 	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()); | ||||
| 
 | ||||
| 		await this.effector.setLayers(this.makeImageEffectorLayers()); | ||||
| 		this.render(); | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue