mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-05-20 02:25:41 +02:00
feat(frontend): 画像編集機能 (#16121)
* wip * wip * wip * wip * Update watermarker.ts * wip * wip * Update watermarker.ts * Update MkUploaderDialog.vue * wip * Update ImageEffector.ts * Update ImageEffector.ts * wip * wip * wip * wip * wip * wip * Update MkRange.vue * Update MkRange.vue * wip * wip * Update MkImageEffectorDialog.vue * Update MkImageEffectorDialog.Layer.vue * wip * Update zoomLines.ts * Update zoomLines.ts * wip * wip * Update ImageEffector.ts * wip * Update ImageEffector.ts * wip * Update ImageEffector.ts * swip * wip * Update ImageEffector.ts * wop * Update MkUploaderDialog.vue * Update ImageEffector.ts * wip * wip * wip * Update def.ts * Update def.ts * test * test * Update manager.ts * Update manager.ts * Update manager.ts * Update manager.ts * Update MkImageEffectorDialog.vue * wip * use WEBGL_lose_context * wip * Update MkUploaderDialog.vue * Update drive.vue * wip * Update MkUploaderDialog.vue * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip
This commit is contained in:
476
packages/frontend/src/utility/image-effector/ImageEffector.ts
Normal file
476
packages/frontend/src/utility/image-effector/ImageEffector.ts
Normal file
@@ -0,0 +1,476 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { getProxiedImageUrl } from '../media-proxy.js';
|
||||
|
||||
type ParamTypeToPrimitive = {
|
||||
'number': number;
|
||||
'number:enum': number;
|
||||
'boolean': boolean;
|
||||
'align': { x: 'left' | 'center' | 'right'; y: 'top' | 'center' | 'bottom'; };
|
||||
'seed': number;
|
||||
'texture': { type: 'text'; text: string | null; } | { type: 'url'; url: string | null; } | null;
|
||||
'color': [r: number, g: number, b: number];
|
||||
};
|
||||
|
||||
type ImageEffectorFxParamDefs = Record<string, {
|
||||
type: keyof ParamTypeToPrimitive;
|
||||
default: any;
|
||||
}>;
|
||||
|
||||
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 = ImageEffectorFxParamDefs, US extends string[] = string[]> = {
|
||||
id: ID;
|
||||
name: string;
|
||||
shader: string;
|
||||
uniforms: US;
|
||||
params: PS,
|
||||
main: (ctx: {
|
||||
gl: WebGL2RenderingContext;
|
||||
program: WebGLProgram;
|
||||
params: {
|
||||
[key in keyof PS]: ParamTypeToPrimitive[PS[key]['type']];
|
||||
};
|
||||
u: Record<US[number], WebGLUniformLocation>;
|
||||
width: number;
|
||||
height: number;
|
||||
textures: Record<string, {
|
||||
texture: WebGLTexture;
|
||||
width: number;
|
||||
height: number;
|
||||
} | null>;
|
||||
}) => void;
|
||||
};
|
||||
|
||||
export type ImageEffectorLayer = {
|
||||
id: string;
|
||||
fxId: string;
|
||||
params: Record<string, any>;
|
||||
};
|
||||
|
||||
function getValue<T extends keyof ParamTypeToPrimitive>(params: Record<string, any>, k: string): ParamTypeToPrimitive[T] {
|
||||
return params[k];
|
||||
}
|
||||
|
||||
export class ImageEffector {
|
||||
private gl: WebGL2RenderingContext;
|
||||
private canvas: HTMLCanvasElement | null = null;
|
||||
private renderTextureProgram: WebGLProgram;
|
||||
private renderInvertedTextureProgram: WebGLProgram;
|
||||
private renderWidth: number;
|
||||
private renderHeight: number;
|
||||
private originalImage: ImageData | ImageBitmap | HTMLImageElement | HTMLCanvasElement;
|
||||
private layers: ImageEffectorLayer[] = [];
|
||||
private originalImageTexture: WebGLTexture;
|
||||
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 paramTextures: Map<string, { texture: WebGLTexture; width: number; height: number; }> = new Map();
|
||||
|
||||
constructor(options: {
|
||||
canvas: HTMLCanvasElement;
|
||||
renderWidth: number;
|
||||
renderHeight: number;
|
||||
image: ImageData | ImageBitmap | HTMLImageElement | HTMLCanvasElement;
|
||||
fxs: ImageEffectorFx[];
|
||||
}) {
|
||||
this.canvas = options.canvas;
|
||||
this.renderWidth = options.renderWidth;
|
||||
this.renderHeight = options.renderHeight;
|
||||
this.originalImage = options.image;
|
||||
this.fxs = options.fxs;
|
||||
|
||||
this.canvas.width = this.renderWidth;
|
||||
this.canvas.height = this.renderHeight;
|
||||
|
||||
const gl = this.canvas.getContext('webgl2', {
|
||||
preserveDrawingBuffer: false,
|
||||
alpha: true,
|
||||
premultipliedAlpha: false,
|
||||
});
|
||||
|
||||
if (gl == null) {
|
||||
throw new Error('Failed to initialize WebGL2 context');
|
||||
}
|
||||
|
||||
this.gl = gl;
|
||||
|
||||
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
|
||||
|
||||
const VERTICES = new Float32Array([-1, -1, -1, 1, 1, 1, -1, -1, 1, 1, 1, -1]);
|
||||
const vertexBuffer = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, VERTICES, gl.STATIC_DRAW);
|
||||
|
||||
this.originalImageTexture = createTexture(gl);
|
||||
gl.activeTexture(gl.TEXTURE0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.originalImageTexture);
|
||||
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
|
||||
in vec2 position;
|
||||
out vec2 in_uv;
|
||||
|
||||
void main() {
|
||||
in_uv = (position + 1.0) / 2.0;
|
||||
gl_Position = vec4(position, 0.0, 1.0);
|
||||
}
|
||||
`, `#version 300 es
|
||||
precision mediump float;
|
||||
|
||||
in vec2 in_uv;
|
||||
uniform sampler2D u_texture;
|
||||
out vec4 out_color;
|
||||
|
||||
void main() {
|
||||
out_color = texture(u_texture, in_uv);
|
||||
}
|
||||
`);
|
||||
|
||||
this.renderInvertedTextureProgram = this.initShaderProgram(`#version 300 es
|
||||
in vec2 position;
|
||||
out vec2 in_uv;
|
||||
|
||||
void main() {
|
||||
in_uv = (position + 1.0) / 2.0;
|
||||
in_uv.y = 1.0 - in_uv.y;
|
||||
gl_Position = vec4(position, 0.0, 1.0);
|
||||
}
|
||||
`, `#version 300 es
|
||||
precision mediump float;
|
||||
|
||||
in vec2 in_uv;
|
||||
uniform sampler2D u_texture;
|
||||
out vec4 out_color;
|
||||
|
||||
void main() {
|
||||
out_color = texture(u_texture, in_uv);
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
public loadShader(type: GLenum, source: string): WebGLShader {
|
||||
const gl = this.gl;
|
||||
|
||||
const shader = gl.createShader(type);
|
||||
if (shader == null) {
|
||||
throw new Error('falied to create shader');
|
||||
}
|
||||
|
||||
gl.shaderSource(shader, source);
|
||||
gl.compileShader(shader);
|
||||
|
||||
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
||||
console.error(`falied to compile shader: ${gl.getShaderInfoLog(shader)}`);
|
||||
gl.deleteShader(shader);
|
||||
throw new Error(`falied to compile shader: ${gl.getShaderInfoLog(shader)}`);
|
||||
}
|
||||
|
||||
return shader;
|
||||
}
|
||||
|
||||
public initShaderProgram(vsSource: string, fsSource: string): WebGLProgram {
|
||||
const gl = this.gl;
|
||||
|
||||
const vertexShader = this.loadShader(gl.VERTEX_SHADER, vsSource);
|
||||
const fragmentShader = this.loadShader(gl.FRAGMENT_SHADER, fsSource);
|
||||
|
||||
const shaderProgram = gl.createProgram();
|
||||
|
||||
gl.attachShader(shaderProgram, vertexShader);
|
||||
gl.attachShader(shaderProgram, fragmentShader);
|
||||
gl.linkProgram(shaderProgram);
|
||||
|
||||
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
|
||||
console.error(`failed to init shader: ${gl.getProgramInfoLog(shaderProgram)}`);
|
||||
throw new Error('failed to init shader');
|
||||
}
|
||||
|
||||
return shaderProgram;
|
||||
}
|
||||
|
||||
private renderLayer(layer: ImageEffectorLayer, preTexture: WebGLTexture) {
|
||||
const gl = this.gl;
|
||||
|
||||
const fx = this.fxs.find(fx => fx.id === layer.fxId);
|
||||
if (fx == null) return;
|
||||
|
||||
const cachedShader = this.shaderCache.get(fx.id);
|
||||
const shaderProgram = cachedShader ?? this.initShaderProgram(`#version 300 es
|
||||
in vec2 position;
|
||||
out vec2 in_uv;
|
||||
|
||||
void main() {
|
||||
in_uv = (position + 1.0) / 2.0;
|
||||
gl_Position = vec4(position, 0.0, 1.0);
|
||||
}
|
||||
`, fx.shader);
|
||||
if (cachedShader == null) {
|
||||
this.shaderCache.set(fx.id, shaderProgram);
|
||||
}
|
||||
|
||||
gl.useProgram(shaderProgram);
|
||||
|
||||
const in_resolution = gl.getUniformLocation(shaderProgram, 'in_resolution');
|
||||
gl.uniform2fv(in_resolution, [this.renderWidth, this.renderHeight]);
|
||||
|
||||
gl.activeTexture(gl.TEXTURE0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, preTexture);
|
||||
const in_texture = gl.getUniformLocation(shaderProgram, 'in_texture');
|
||||
gl.uniform1i(in_texture, 0);
|
||||
|
||||
fx.main({
|
||||
gl: gl,
|
||||
program: shaderProgram,
|
||||
params: Object.fromEntries(
|
||||
Object.entries(fx.params).map(([key, param]) => {
|
||||
return [key, layer.params[key] ?? param.default];
|
||||
}),
|
||||
),
|
||||
u: Object.fromEntries(fx.uniforms.map(u => [u, gl.getUniformLocation(shaderProgram, 'u_' + u)!])),
|
||||
width: this.renderWidth,
|
||||
height: this.renderHeight,
|
||||
textures: Object.fromEntries(
|
||||
Object.entries(fx.params).map(([k, v]) => {
|
||||
if (v.type !== 'texture') return [k, null];
|
||||
const param = getValue<typeof v.type>(layer.params, k);
|
||||
if (param == null) return [k, null];
|
||||
const texture = this.paramTextures.get(this.getTextureKeyForParam(param)) ?? null;
|
||||
return [k, texture];
|
||||
})),
|
||||
});
|
||||
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const gl = this.gl;
|
||||
|
||||
{
|
||||
gl.activeTexture(gl.TEXTURE0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.originalImageTexture);
|
||||
|
||||
gl.useProgram(this.renderTextureProgram);
|
||||
const u_texture = gl.getUniformLocation(this.renderTextureProgram, 'u_texture');
|
||||
gl.uniform1i(u_texture, 0);
|
||||
const u_resolution = gl.getUniformLocation(this.renderTextureProgram, 'u_resolution');
|
||||
gl.uniform2fv(u_resolution, [this.renderWidth, this.renderHeight]);
|
||||
const positionLocation = gl.getAttribLocation(this.renderTextureProgram, 'position');
|
||||
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
|
||||
gl.enableVertexAttribArray(positionLocation);
|
||||
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||||
}
|
||||
|
||||
// --------------------
|
||||
|
||||
let preTexture = this.originalImageTexture;
|
||||
|
||||
for (const layer of this.layers) {
|
||||
const cachedResultTexture = this.perLayerResultTextures.get(layer.id);
|
||||
const resultTexture = cachedResultTexture ?? createTexture(gl);
|
||||
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;
|
||||
}
|
||||
|
||||
// --------------------
|
||||
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
||||
gl.useProgram(this.renderInvertedTextureProgram);
|
||||
|
||||
gl.activeTexture(gl.TEXTURE0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, preTexture);
|
||||
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||||
}
|
||||
|
||||
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 of Object.keys(layer.params)) {
|
||||
const paramDef = fx.params[k];
|
||||
if (paramDef == null) continue;
|
||||
if (paramDef.type !== 'texture') continue;
|
||||
const v = getValue<typeof paramDef.type>(layer.params, k);
|
||||
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 changeResolution(width: number, height: number) {
|
||||
this.renderWidth = width;
|
||||
this.renderHeight = height;
|
||||
if (this.canvas) {
|
||||
this.canvas.width = this.renderWidth;
|
||||
this.canvas.height = this.renderHeight;
|
||||
}
|
||||
this.gl.viewport(0, 0, this.renderWidth, this.renderHeight);
|
||||
}
|
||||
|
||||
private getTextureKeyForParam(v: ParamTypeToPrimitive['texture']) {
|
||||
if (v == null) return '';
|
||||
return v.type === 'text' ? `text:${v.text}` : v.type === 'url' ? `url:${v.url}` : '';
|
||||
}
|
||||
|
||||
/*
|
||||
* disposeCanvas = true だとloseContextを呼ぶため、コンストラクタで渡されたcanvasも再利用不可になるので注意
|
||||
*/
|
||||
public destroy(disposeCanvas = true) {
|
||||
for (const shader of this.shaderCache.values()) {
|
||||
this.gl.deleteProgram(shader);
|
||||
}
|
||||
this.shaderCache.clear();
|
||||
|
||||
for (const texture of this.perLayerResultTextures.values()) {
|
||||
this.gl.deleteTexture(texture);
|
||||
}
|
||||
this.perLayerResultTextures.clear();
|
||||
|
||||
for (const framebuffer of this.perLayerResultFrameBuffers.values()) {
|
||||
this.gl.deleteFramebuffer(framebuffer);
|
||||
}
|
||||
this.perLayerResultFrameBuffers.clear();
|
||||
|
||||
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);
|
||||
|
||||
if (disposeCanvas) {
|
||||
const loseContextExt = this.gl.getExtension('WEBGL_lose_context');
|
||||
if (loseContextExt) loseContextExt.loseContext();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.MIRRORED_REPEAT);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_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対策
|
||||
}).catch(() => null);
|
||||
|
||||
if (image == null) return null;
|
||||
|
||||
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;
|
||||
}
|
||||
37
packages/frontend/src/utility/image-effector/fxs.ts
Normal file
37
packages/frontend/src/utility/image-effector/fxs.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { FX_checker } from './fxs/checker.js';
|
||||
import { FX_chromaticAberration } from './fxs/chromaticAberration.js';
|
||||
import { FX_colorClamp } from './fxs/colorClamp.js';
|
||||
import { FX_colorClampAdvanced } from './fxs/colorClampAdvanced.js';
|
||||
import { FX_distort } from './fxs/distort.js';
|
||||
import { FX_polkadot } from './fxs/polkadot.js';
|
||||
import { FX_glitch } from './fxs/glitch.js';
|
||||
import { FX_grayscale } from './fxs/grayscale.js';
|
||||
import { FX_invert } from './fxs/invert.js';
|
||||
import { FX_mirror } from './fxs/mirror.js';
|
||||
import { FX_stripe } from './fxs/stripe.js';
|
||||
import { FX_threshold } from './fxs/threshold.js';
|
||||
import { FX_watermarkPlacement } from './fxs/watermarkPlacement.js';
|
||||
import { FX_zoomLines } from './fxs/zoomLines.js';
|
||||
import type { ImageEffectorFx } from './ImageEffector.js';
|
||||
|
||||
export const FXS = [
|
||||
FX_watermarkPlacement,
|
||||
FX_chromaticAberration,
|
||||
FX_glitch,
|
||||
FX_mirror,
|
||||
FX_invert,
|
||||
FX_grayscale,
|
||||
FX_colorClamp,
|
||||
FX_colorClampAdvanced,
|
||||
FX_distort,
|
||||
FX_threshold,
|
||||
FX_zoomLines,
|
||||
FX_stripe,
|
||||
FX_polkadot,
|
||||
FX_checker,
|
||||
] as const satisfies ImageEffectorFx<string, any>[];
|
||||
87
packages/frontend/src/utility/image-effector/fxs/checker.ts
Normal file
87
packages/frontend/src/utility/image-effector/fxs/checker.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineImageEffectorFx } from '../ImageEffector.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const shader = `#version 300 es
|
||||
precision mediump float;
|
||||
|
||||
const float PI = 3.141592653589793;
|
||||
const float TWO_PI = 6.283185307179586;
|
||||
const float HALF_PI = 1.5707963267948966;
|
||||
|
||||
in vec2 in_uv;
|
||||
uniform sampler2D in_texture;
|
||||
uniform vec2 in_resolution;
|
||||
uniform float u_angle;
|
||||
uniform float u_scale;
|
||||
uniform vec3 u_color;
|
||||
uniform float u_opacity;
|
||||
out vec4 out_color;
|
||||
|
||||
void main() {
|
||||
vec4 in_color = texture(in_texture, in_uv);
|
||||
float x_ratio = max(in_resolution.x / in_resolution.y, 1.0);
|
||||
float y_ratio = max(in_resolution.y / in_resolution.x, 1.0);
|
||||
|
||||
float angle = -(u_angle * PI);
|
||||
vec2 centeredUv = (in_uv - vec2(0.5, 0.5)) * vec2(x_ratio, y_ratio);
|
||||
vec2 rotatedUV = vec2(
|
||||
centeredUv.x * cos(angle) - centeredUv.y * sin(angle),
|
||||
centeredUv.x * sin(angle) + centeredUv.y * cos(angle)
|
||||
);
|
||||
|
||||
float fmodResult = mod(floor(u_scale * rotatedUV.x) + floor(u_scale * rotatedUV.y), 2.0);
|
||||
float fin = max(sign(fmodResult), 0.0);
|
||||
|
||||
out_color = vec4(
|
||||
mix(in_color.r, u_color.r, fin * u_opacity),
|
||||
mix(in_color.g, u_color.g, fin * u_opacity),
|
||||
mix(in_color.b, u_color.b, fin * u_opacity),
|
||||
in_color.a
|
||||
);
|
||||
}
|
||||
`;
|
||||
|
||||
export const FX_checker = defineImageEffectorFx({
|
||||
id: 'checker' as const,
|
||||
name: i18n.ts._imageEffector._fxs.checker,
|
||||
shader,
|
||||
uniforms: ['angle', 'scale', 'color', 'opacity'] as const,
|
||||
params: {
|
||||
angle: {
|
||||
type: 'number' as const,
|
||||
default: 0,
|
||||
min: -1.0,
|
||||
max: 1.0,
|
||||
step: 0.01,
|
||||
},
|
||||
scale: {
|
||||
type: 'number' as const,
|
||||
default: 3.0,
|
||||
min: 1.0,
|
||||
max: 10.0,
|
||||
step: 0.1,
|
||||
},
|
||||
color: {
|
||||
type: 'color' as const,
|
||||
default: [1, 1, 1],
|
||||
},
|
||||
opacity: {
|
||||
type: 'number' as const,
|
||||
default: 0.5,
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
step: 0.01,
|
||||
},
|
||||
},
|
||||
main: ({ gl, u, params }) => {
|
||||
gl.uniform1f(u.angle, params.angle / 2);
|
||||
gl.uniform1f(u.scale, params.scale * params.scale);
|
||||
gl.uniform3f(u.color, params.color[0], params.color[1], params.color[2]);
|
||||
gl.uniform1f(u.opacity, params.opacity);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineImageEffectorFx } from '../ImageEffector.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const shader = `#version 300 es
|
||||
precision mediump float;
|
||||
|
||||
in vec2 in_uv;
|
||||
uniform sampler2D in_texture;
|
||||
uniform vec2 in_resolution;
|
||||
out vec4 out_color;
|
||||
uniform float u_amount;
|
||||
uniform float u_start;
|
||||
uniform bool u_normalize;
|
||||
|
||||
void main() {
|
||||
int samples = 64;
|
||||
float r_strength = 1.0;
|
||||
float g_strength = 1.5;
|
||||
float b_strength = 2.0;
|
||||
|
||||
vec2 size = vec2(in_resolution.x, in_resolution.y);
|
||||
|
||||
vec4 accumulator = vec4(0.0);
|
||||
float normalisedValue = length((in_uv - 0.5) * 2.0);
|
||||
float strength = clamp((normalisedValue - u_start) * (1.0 / (1.0 - u_start)), 0.0, 1.0);
|
||||
|
||||
vec2 vector = (u_normalize ? normalize(in_uv - vec2(0.5)) : in_uv - vec2(0.5));
|
||||
vec2 velocity = vector * strength * u_amount;
|
||||
|
||||
vec2 rOffset = -vector * strength * (u_amount * r_strength);
|
||||
vec2 gOffset = -vector * strength * (u_amount * g_strength);
|
||||
vec2 bOffset = -vector * strength * (u_amount * b_strength);
|
||||
|
||||
for (int i = 0; i < samples; i++) {
|
||||
accumulator.r += texture(in_texture, in_uv + rOffset).r;
|
||||
rOffset -= velocity / float(samples);
|
||||
|
||||
accumulator.g += texture(in_texture, in_uv + gOffset).g;
|
||||
gOffset -= velocity / float(samples);
|
||||
|
||||
accumulator.b += texture(in_texture, in_uv + bOffset).b;
|
||||
bOffset -= velocity / float(samples);
|
||||
}
|
||||
|
||||
out_color = vec4(vec3(accumulator / float(samples)), 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
export const FX_chromaticAberration = defineImageEffectorFx({
|
||||
id: 'chromaticAberration' as const,
|
||||
name: i18n.ts._imageEffector._fxs.chromaticAberration,
|
||||
shader,
|
||||
uniforms: ['amount', 'start', 'normalize'] as const,
|
||||
params: {
|
||||
normalize: {
|
||||
type: 'boolean' as const,
|
||||
default: false,
|
||||
},
|
||||
amount: {
|
||||
type: 'number' as const,
|
||||
default: 0.1,
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
step: 0.01,
|
||||
},
|
||||
},
|
||||
main: ({ gl, u, params }) => {
|
||||
gl.uniform1f(u.amount, params.amount);
|
||||
gl.uniform1i(u.normalize, params.normalize ? 1 : 0);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineImageEffectorFx } from '../ImageEffector.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const shader = `#version 300 es
|
||||
precision mediump float;
|
||||
|
||||
in vec2 in_uv;
|
||||
uniform sampler2D in_texture;
|
||||
uniform vec2 in_resolution;
|
||||
uniform float u_max;
|
||||
uniform float u_min;
|
||||
out vec4 out_color;
|
||||
|
||||
void main() {
|
||||
vec4 in_color = texture(in_texture, in_uv);
|
||||
float r = min(max(in_color.r, u_min), u_max);
|
||||
float g = min(max(in_color.g, u_min), u_max);
|
||||
float b = min(max(in_color.b, u_min), u_max);
|
||||
out_color = vec4(r, g, b, in_color.a);
|
||||
}
|
||||
`;
|
||||
|
||||
export const FX_colorClamp = defineImageEffectorFx({
|
||||
id: 'colorClamp' as const,
|
||||
name: i18n.ts._imageEffector._fxs.colorClamp,
|
||||
shader,
|
||||
uniforms: ['max', 'min'] as const,
|
||||
params: {
|
||||
max: {
|
||||
type: 'number' as const,
|
||||
default: 1.0,
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
step: 0.01,
|
||||
},
|
||||
min: {
|
||||
type: 'number' as const,
|
||||
default: -1.0,
|
||||
min: -1.0,
|
||||
max: 0.0,
|
||||
step: 0.01,
|
||||
},
|
||||
},
|
||||
main: ({ gl, u, params }) => {
|
||||
gl.uniform1f(u.max, params.max);
|
||||
gl.uniform1f(u.min, 1.0 + params.min);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineImageEffectorFx } from '../ImageEffector.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const shader = `#version 300 es
|
||||
precision mediump float;
|
||||
|
||||
in vec2 in_uv;
|
||||
uniform sampler2D in_texture;
|
||||
uniform vec2 in_resolution;
|
||||
uniform float u_rMax;
|
||||
uniform float u_rMin;
|
||||
uniform float u_gMax;
|
||||
uniform float u_gMin;
|
||||
uniform float u_bMax;
|
||||
uniform float u_bMin;
|
||||
out vec4 out_color;
|
||||
|
||||
void main() {
|
||||
vec4 in_color = texture(in_texture, in_uv);
|
||||
float r = min(max(in_color.r, u_rMin), u_rMax);
|
||||
float g = min(max(in_color.g, u_gMin), u_gMax);
|
||||
float b = min(max(in_color.b, u_bMin), u_bMax);
|
||||
out_color = vec4(r, g, b, in_color.a);
|
||||
}
|
||||
`;
|
||||
|
||||
export const FX_colorClampAdvanced = defineImageEffectorFx({
|
||||
id: 'colorClampAdvanced' as const,
|
||||
name: i18n.ts._imageEffector._fxs.colorClampAdvanced,
|
||||
shader,
|
||||
uniforms: ['rMax', 'rMin', 'gMax', 'gMin', 'bMax', 'bMin'] as const,
|
||||
params: {
|
||||
rMax: {
|
||||
type: 'number' as const,
|
||||
default: 1.0,
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
step: 0.01,
|
||||
},
|
||||
rMin: {
|
||||
type: 'number' as const,
|
||||
default: -1.0,
|
||||
min: -1.0,
|
||||
max: 0.0,
|
||||
step: 0.01,
|
||||
},
|
||||
gMax: {
|
||||
type: 'number' as const,
|
||||
default: 1.0,
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
step: 0.01,
|
||||
},
|
||||
gMin: {
|
||||
type: 'number' as const,
|
||||
default: -1.0,
|
||||
min: -1.0,
|
||||
max: 0.0,
|
||||
step: 0.01,
|
||||
},
|
||||
bMax: {
|
||||
type: 'number' as const,
|
||||
default: 1.0,
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
step: 0.01,
|
||||
},
|
||||
bMin: {
|
||||
type: 'number' as const,
|
||||
default: -1.0,
|
||||
min: -1.0,
|
||||
max: 0.0,
|
||||
step: 0.01,
|
||||
},
|
||||
},
|
||||
main: ({ gl, u, params }) => {
|
||||
gl.uniform1f(u.rMax, params.rMax);
|
||||
gl.uniform1f(u.rMin, 1.0 + params.rMin);
|
||||
gl.uniform1f(u.gMax, params.gMax);
|
||||
gl.uniform1f(u.gMin, 1.0 + params.gMin);
|
||||
gl.uniform1f(u.bMax, params.bMax);
|
||||
gl.uniform1f(u.bMin, 1.0 + params.bMin);
|
||||
},
|
||||
});
|
||||
71
packages/frontend/src/utility/image-effector/fxs/distort.ts
Normal file
71
packages/frontend/src/utility/image-effector/fxs/distort.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineImageEffectorFx } from '../ImageEffector.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const shader = `#version 300 es
|
||||
precision mediump float;
|
||||
|
||||
in vec2 in_uv;
|
||||
uniform sampler2D in_texture;
|
||||
uniform vec2 in_resolution;
|
||||
uniform float u_phase;
|
||||
uniform float u_frequency;
|
||||
uniform float u_strength;
|
||||
uniform int u_direction; // 0: vertical, 1: horizontal
|
||||
out vec4 out_color;
|
||||
|
||||
void main() {
|
||||
float v = u_direction == 0 ?
|
||||
sin(u_phase + in_uv.y * u_frequency) * u_strength :
|
||||
sin(u_phase + in_uv.x * u_frequency) * u_strength;
|
||||
vec4 in_color = u_direction == 0 ?
|
||||
texture(in_texture, vec2(in_uv.x + v, in_uv.y)) :
|
||||
texture(in_texture, vec2(in_uv.x, in_uv.y + v));
|
||||
out_color = in_color;
|
||||
}
|
||||
`;
|
||||
|
||||
export const FX_distort = defineImageEffectorFx({
|
||||
id: 'distort' as const,
|
||||
name: i18n.ts._imageEffector._fxs.distort,
|
||||
shader,
|
||||
uniforms: ['phase', 'frequency', 'strength', 'direction'] as const,
|
||||
params: {
|
||||
direction: {
|
||||
type: 'number:enum' as const,
|
||||
enum: [{ value: 0, label: 'v' }, { value: 1, label: 'h' }],
|
||||
default: 0,
|
||||
},
|
||||
phase: {
|
||||
type: 'number' as const,
|
||||
default: 50.0,
|
||||
min: 0.0,
|
||||
max: 100,
|
||||
step: 0.01,
|
||||
},
|
||||
frequency: {
|
||||
type: 'number' as const,
|
||||
default: 50,
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 0.1,
|
||||
},
|
||||
strength: {
|
||||
type: 'number' as const,
|
||||
default: 0.1,
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.01,
|
||||
},
|
||||
},
|
||||
main: ({ gl, u, params }) => {
|
||||
gl.uniform1f(u.phase, params.phase / 10);
|
||||
gl.uniform1f(u.frequency, params.frequency);
|
||||
gl.uniform1f(u.strength, params.strength);
|
||||
gl.uniform1i(u.direction, params.direction);
|
||||
},
|
||||
});
|
||||
96
packages/frontend/src/utility/image-effector/fxs/glitch.ts
Normal file
96
packages/frontend/src/utility/image-effector/fxs/glitch.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import seedrandom from 'seedrandom';
|
||||
import { defineImageEffectorFx } from '../ImageEffector.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const shader = `#version 300 es
|
||||
precision mediump float;
|
||||
|
||||
in vec2 in_uv;
|
||||
uniform sampler2D in_texture;
|
||||
uniform vec2 in_resolution;
|
||||
uniform int u_amount;
|
||||
uniform float u_shiftStrengths[128];
|
||||
uniform float u_shiftOrigins[128];
|
||||
uniform float u_shiftHeights[128];
|
||||
uniform float u_channelShift;
|
||||
out vec4 out_color;
|
||||
|
||||
void main() {
|
||||
float v = 0.0;
|
||||
|
||||
for (int i = 0; i < u_amount; i++) {
|
||||
if (in_uv.y > (u_shiftOrigins[i] - u_shiftHeights[i]) && in_uv.y < (u_shiftOrigins[i] + u_shiftHeights[i])) {
|
||||
v += u_shiftStrengths[i];
|
||||
}
|
||||
}
|
||||
|
||||
float r = texture(in_texture, vec2(in_uv.x + (v * (1.0 + u_channelShift)), in_uv.y)).r;
|
||||
float g = texture(in_texture, vec2(in_uv.x + v, in_uv.y)).g;
|
||||
float b = texture(in_texture, vec2(in_uv.x + (v * (1.0 + (u_channelShift / 2.0))), in_uv.y)).b;
|
||||
float a = texture(in_texture, vec2(in_uv.x + v, in_uv.y)).a;
|
||||
out_color = vec4(r, g, b, a);
|
||||
}
|
||||
`;
|
||||
|
||||
export const FX_glitch = defineImageEffectorFx({
|
||||
id: 'glitch' as const,
|
||||
name: i18n.ts._imageEffector._fxs.glitch,
|
||||
shader,
|
||||
uniforms: ['amount', 'channelShift'] as const,
|
||||
params: {
|
||||
amount: {
|
||||
type: 'number' as const,
|
||||
default: 3,
|
||||
min: 1,
|
||||
max: 100,
|
||||
step: 1,
|
||||
},
|
||||
strength: {
|
||||
type: 'number' as const,
|
||||
default: 5,
|
||||
min: -100,
|
||||
max: 100,
|
||||
step: 0.01,
|
||||
},
|
||||
size: {
|
||||
type: 'number' as const,
|
||||
default: 20,
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 0.01,
|
||||
},
|
||||
channelShift: {
|
||||
type: 'number' as const,
|
||||
default: 0.5,
|
||||
min: 0,
|
||||
max: 10,
|
||||
step: 0.01,
|
||||
},
|
||||
seed: {
|
||||
type: 'seed' as const,
|
||||
default: 100,
|
||||
},
|
||||
},
|
||||
main: ({ gl, program, u, params }) => {
|
||||
gl.uniform1i(u.amount, params.amount);
|
||||
gl.uniform1f(u.channelShift, params.channelShift);
|
||||
|
||||
const rnd = seedrandom(params.seed.toString());
|
||||
|
||||
for (let i = 0; i < params.amount; i++) {
|
||||
const o = gl.getUniformLocation(program, `u_shiftOrigins[${i.toString()}]`);
|
||||
gl.uniform1f(o, rnd());
|
||||
|
||||
const s = gl.getUniformLocation(program, `u_shiftStrengths[${i.toString()}]`);
|
||||
gl.uniform1f(s, (1 - (rnd() * 2)) * (params.strength / 100));
|
||||
|
||||
const h = gl.getUniformLocation(program, `u_shiftHeights[${i.toString()}]`);
|
||||
gl.uniform1f(h, rnd() * (params.size / 100));
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineImageEffectorFx } from '../ImageEffector.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const shader = `#version 300 es
|
||||
precision mediump float;
|
||||
|
||||
in vec2 in_uv;
|
||||
uniform sampler2D in_texture;
|
||||
uniform vec2 in_resolution;
|
||||
out vec4 out_color;
|
||||
|
||||
float getBrightness(vec4 color) {
|
||||
return (color.r + color.g + color.b) / 3.0;
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec4 in_color = texture(in_texture, in_uv);
|
||||
float brightness = getBrightness(in_color);
|
||||
out_color = vec4(brightness, brightness, brightness, in_color.a);
|
||||
}
|
||||
`;
|
||||
|
||||
export const FX_grayscale = defineImageEffectorFx({
|
||||
id: 'grayscale' as const,
|
||||
name: i18n.ts._imageEffector._fxs.grayscale,
|
||||
shader,
|
||||
uniforms: [] as const,
|
||||
params: {
|
||||
},
|
||||
main: ({ gl, params }) => {
|
||||
},
|
||||
});
|
||||
53
packages/frontend/src/utility/image-effector/fxs/invert.ts
Normal file
53
packages/frontend/src/utility/image-effector/fxs/invert.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineImageEffectorFx } from '../ImageEffector.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const shader = `#version 300 es
|
||||
precision mediump float;
|
||||
|
||||
in vec2 in_uv;
|
||||
uniform sampler2D in_texture;
|
||||
uniform vec2 in_resolution;
|
||||
uniform bool u_r;
|
||||
uniform bool u_g;
|
||||
uniform bool u_b;
|
||||
out vec4 out_color;
|
||||
|
||||
void main() {
|
||||
vec4 in_color = texture(in_texture, in_uv);
|
||||
out_color.r = u_r ? 1.0 - in_color.r : in_color.r;
|
||||
out_color.g = u_g ? 1.0 - in_color.g : in_color.g;
|
||||
out_color.b = u_b ? 1.0 - in_color.b : in_color.b;
|
||||
out_color.a = in_color.a;
|
||||
}
|
||||
`;
|
||||
|
||||
export const FX_invert = defineImageEffectorFx({
|
||||
id: 'invert' as const,
|
||||
name: i18n.ts._imageEffector._fxs.invert,
|
||||
shader,
|
||||
uniforms: ['r', 'g', 'b'] as const,
|
||||
params: {
|
||||
r: {
|
||||
type: 'boolean' as const,
|
||||
default: true,
|
||||
},
|
||||
g: {
|
||||
type: 'boolean' as const,
|
||||
default: true,
|
||||
},
|
||||
b: {
|
||||
type: 'boolean' as const,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
main: ({ gl, u, params }) => {
|
||||
gl.uniform1i(u.r, params.r ? 1 : 0);
|
||||
gl.uniform1i(u.g, params.g ? 1 : 0);
|
||||
gl.uniform1i(u.b, params.b ? 1 : 0);
|
||||
},
|
||||
});
|
||||
58
packages/frontend/src/utility/image-effector/fxs/mirror.ts
Normal file
58
packages/frontend/src/utility/image-effector/fxs/mirror.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineImageEffectorFx } from '../ImageEffector.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const shader = `#version 300 es
|
||||
precision mediump float;
|
||||
|
||||
in vec2 in_uv;
|
||||
uniform sampler2D in_texture;
|
||||
uniform vec2 in_resolution;
|
||||
uniform int u_h;
|
||||
uniform int u_v;
|
||||
out vec4 out_color;
|
||||
|
||||
void main() {
|
||||
vec2 uv = in_uv;
|
||||
if (u_h == -1 && in_uv.x > 0.5) {
|
||||
uv.x = 1.0 - uv.x;
|
||||
}
|
||||
if (u_h == 1 && in_uv.x < 0.5) {
|
||||
uv.x = 1.0 - uv.x;
|
||||
}
|
||||
if (u_v == -1 && in_uv.y > 0.5) {
|
||||
uv.y = 1.0 - uv.y;
|
||||
}
|
||||
if (u_v == 1 && in_uv.y < 0.5) {
|
||||
uv.y = 1.0 - uv.y;
|
||||
}
|
||||
out_color = texture(in_texture, uv);
|
||||
}
|
||||
`;
|
||||
|
||||
export const FX_mirror = defineImageEffectorFx({
|
||||
id: 'mirror' as const,
|
||||
name: i18n.ts._imageEffector._fxs.mirror,
|
||||
shader,
|
||||
uniforms: ['h', 'v'] as const,
|
||||
params: {
|
||||
h: {
|
||||
type: 'number:enum' as const,
|
||||
enum: [{ value: -1, label: '<-' }, { value: 0, label: '|' }, { value: 1, label: '->' }],
|
||||
default: -1,
|
||||
},
|
||||
v: {
|
||||
type: 'number:enum' as const,
|
||||
enum: [{ value: -1, label: '^' }, { value: 0, label: '-' }, { value: 1, label: 'v' }],
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
main: ({ gl, u, params }) => {
|
||||
gl.uniform1i(u.h, params.h);
|
||||
gl.uniform1i(u.v, params.v);
|
||||
},
|
||||
});
|
||||
151
packages/frontend/src/utility/image-effector/fxs/polkadot.ts
Normal file
151
packages/frontend/src/utility/image-effector/fxs/polkadot.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineImageEffectorFx } from '../ImageEffector.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const shader = `#version 300 es
|
||||
precision mediump float;
|
||||
|
||||
const float PI = 3.141592653589793;
|
||||
const float TWO_PI = 6.283185307179586;
|
||||
const float HALF_PI = 1.5707963267948966;
|
||||
|
||||
in vec2 in_uv;
|
||||
uniform sampler2D in_texture;
|
||||
uniform vec2 in_resolution;
|
||||
uniform float u_angle;
|
||||
uniform float u_scale;
|
||||
uniform float u_major_radius;
|
||||
uniform float u_major_opacity;
|
||||
uniform float u_minor_divisions;
|
||||
uniform float u_minor_radius;
|
||||
uniform float u_minor_opacity;
|
||||
uniform vec3 u_color;
|
||||
out vec4 out_color;
|
||||
|
||||
void main() {
|
||||
vec4 in_color = texture(in_texture, in_uv);
|
||||
float x_ratio = max(in_resolution.x / in_resolution.y, 1.0);
|
||||
float y_ratio = max(in_resolution.y / in_resolution.x, 1.0);
|
||||
|
||||
float angle = -(u_angle * PI);
|
||||
vec2 centeredUv = (in_uv - vec2(0.5, 0.5)) * vec2(x_ratio, y_ratio);
|
||||
vec2 rotatedUV = vec2(
|
||||
centeredUv.x * cos(angle) - centeredUv.y * sin(angle),
|
||||
centeredUv.x * sin(angle) + centeredUv.y * cos(angle)
|
||||
);
|
||||
|
||||
float major_modX = mod(rotatedUV.x, (1.0 / u_scale));
|
||||
float major_modY = mod(rotatedUV.y, (1.0 / u_scale));
|
||||
float major_threshold = ((u_major_radius / 2.0) / u_scale);
|
||||
if (
|
||||
length(vec2(major_modX, major_modY)) < major_threshold ||
|
||||
length(vec2((1.0 / u_scale) - major_modX, major_modY)) < major_threshold ||
|
||||
length(vec2(major_modX, (1.0 / u_scale) - major_modY)) < major_threshold ||
|
||||
length(vec2((1.0 / u_scale) - major_modX, (1.0 / u_scale) - major_modY)) < major_threshold
|
||||
) {
|
||||
out_color = vec4(
|
||||
mix(in_color.r, u_color.r, u_major_opacity),
|
||||
mix(in_color.g, u_color.g, u_major_opacity),
|
||||
mix(in_color.b, u_color.b, u_major_opacity),
|
||||
in_color.a
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
float minor_modX = mod(rotatedUV.x, (1.0 / u_scale / u_minor_divisions));
|
||||
float minor_modY = mod(rotatedUV.y, (1.0 / u_scale / u_minor_divisions));
|
||||
float minor_threshold = ((u_minor_radius / 2.0) / (u_minor_divisions * u_scale));
|
||||
if (
|
||||
length(vec2(minor_modX, minor_modY)) < minor_threshold ||
|
||||
length(vec2((1.0 / u_scale / u_minor_divisions) - minor_modX, minor_modY)) < minor_threshold ||
|
||||
length(vec2(minor_modX, (1.0 / u_scale / u_minor_divisions) - minor_modY)) < minor_threshold ||
|
||||
length(vec2((1.0 / u_scale / u_minor_divisions) - minor_modX, (1.0 / u_scale / u_minor_divisions) - minor_modY)) < minor_threshold
|
||||
) {
|
||||
out_color = vec4(
|
||||
mix(in_color.r, u_color.r, u_minor_opacity),
|
||||
mix(in_color.g, u_color.g, u_minor_opacity),
|
||||
mix(in_color.b, u_color.b, u_minor_opacity),
|
||||
in_color.a
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
out_color = in_color;
|
||||
}
|
||||
`;
|
||||
|
||||
export const FX_polkadot = defineImageEffectorFx({
|
||||
id: 'polkadot' as const,
|
||||
name: i18n.ts._imageEffector._fxs.polkadot,
|
||||
shader,
|
||||
uniforms: ['angle', 'scale', 'major_radius', 'major_opacity', 'minor_divisions', 'minor_radius', 'minor_opacity', 'color'] as const,
|
||||
params: {
|
||||
angle: {
|
||||
type: 'number' as const,
|
||||
default: 0,
|
||||
min: -1.0,
|
||||
max: 1.0,
|
||||
step: 0.01,
|
||||
},
|
||||
scale: {
|
||||
type: 'number' as const,
|
||||
default: 3.0,
|
||||
min: 1.0,
|
||||
max: 10.0,
|
||||
step: 0.1,
|
||||
},
|
||||
majorRadius: {
|
||||
type: 'number' as const,
|
||||
default: 0.1,
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
step: 0.01,
|
||||
},
|
||||
majorOpacity: {
|
||||
type: 'number' as const,
|
||||
default: 0.75,
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
step: 0.01,
|
||||
},
|
||||
minorDivisions: {
|
||||
type: 'number' as const,
|
||||
default: 4,
|
||||
min: 0,
|
||||
max: 16,
|
||||
step: 1,
|
||||
},
|
||||
minorRadius: {
|
||||
type: 'number' as const,
|
||||
default: 0.25,
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
step: 0.01,
|
||||
},
|
||||
minorOpacity: {
|
||||
type: 'number' as const,
|
||||
default: 0.5,
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
step: 0.01,
|
||||
},
|
||||
color: {
|
||||
type: 'color' as const,
|
||||
default: [1, 1, 1],
|
||||
},
|
||||
},
|
||||
main: ({ gl, u, params }) => {
|
||||
gl.uniform1f(u.angle, params.angle / 2);
|
||||
gl.uniform1f(u.scale, params.scale * params.scale);
|
||||
gl.uniform1f(u.major_radius, params.majorRadius);
|
||||
gl.uniform1f(u.major_opacity, params.majorOpacity);
|
||||
gl.uniform1f(u.minor_divisions, params.minorDivisions);
|
||||
gl.uniform1f(u.minor_radius, params.minorRadius);
|
||||
gl.uniform3f(u.color, params.color[0], params.color[1], params.color[2]);
|
||||
gl.uniform1f(u.minor_opacity, params.minorOpacity);
|
||||
},
|
||||
});
|
||||
98
packages/frontend/src/utility/image-effector/fxs/stripe.ts
Normal file
98
packages/frontend/src/utility/image-effector/fxs/stripe.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineImageEffectorFx } from '../ImageEffector.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const shader = `#version 300 es
|
||||
precision mediump float;
|
||||
|
||||
const float PI = 3.141592653589793;
|
||||
const float TWO_PI = 6.283185307179586;
|
||||
const float HALF_PI = 1.5707963267948966;
|
||||
|
||||
in vec2 in_uv;
|
||||
uniform sampler2D in_texture;
|
||||
uniform vec2 in_resolution;
|
||||
uniform float u_angle;
|
||||
uniform float u_frequency;
|
||||
uniform float u_phase;
|
||||
uniform float u_threshold;
|
||||
uniform vec3 u_color;
|
||||
uniform float u_opacity;
|
||||
out vec4 out_color;
|
||||
|
||||
void main() {
|
||||
vec4 in_color = texture(in_texture, in_uv);
|
||||
float x_ratio = max(in_resolution.x / in_resolution.y, 1.0);
|
||||
float y_ratio = max(in_resolution.y / in_resolution.x, 1.0);
|
||||
|
||||
float angle = -(u_angle * PI);
|
||||
vec2 centeredUv = (in_uv - vec2(0.5, 0.5)) * vec2(x_ratio, y_ratio);
|
||||
vec2 rotatedUV = vec2(
|
||||
centeredUv.x * cos(angle) - centeredUv.y * sin(angle),
|
||||
centeredUv.x * sin(angle) + centeredUv.y * cos(angle)
|
||||
);
|
||||
|
||||
float phase = u_phase * TWO_PI;
|
||||
float value = (1.0 + sin((rotatedUV.x * u_frequency - HALF_PI) + phase)) / 2.0;
|
||||
value = value < u_threshold ? 1.0 : 0.0;
|
||||
out_color = vec4(
|
||||
mix(in_color.r, u_color.r, value * u_opacity),
|
||||
mix(in_color.g, u_color.g, value * u_opacity),
|
||||
mix(in_color.b, u_color.b, value * u_opacity),
|
||||
in_color.a
|
||||
);
|
||||
}
|
||||
`;
|
||||
|
||||
export const FX_stripe = defineImageEffectorFx({
|
||||
id: 'stripe' as const,
|
||||
name: i18n.ts._imageEffector._fxs.stripe,
|
||||
shader,
|
||||
uniforms: ['angle', 'frequency', 'phase', 'threshold', 'color', 'opacity'] as const,
|
||||
params: {
|
||||
angle: {
|
||||
type: 'number' as const,
|
||||
default: 0.5,
|
||||
min: -1.0,
|
||||
max: 1.0,
|
||||
step: 0.01,
|
||||
},
|
||||
frequency: {
|
||||
type: 'number' as const,
|
||||
default: 10.0,
|
||||
min: 1.0,
|
||||
max: 30.0,
|
||||
step: 0.1,
|
||||
},
|
||||
threshold: {
|
||||
type: 'number' as const,
|
||||
default: 0.1,
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
step: 0.01,
|
||||
},
|
||||
color: {
|
||||
type: 'color' as const,
|
||||
default: [1, 1, 1],
|
||||
},
|
||||
opacity: {
|
||||
type: 'number' as const,
|
||||
default: 0.5,
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
step: 0.01,
|
||||
},
|
||||
},
|
||||
main: ({ gl, u, params }) => {
|
||||
gl.uniform1f(u.angle, params.angle / 2);
|
||||
gl.uniform1f(u.frequency, params.frequency * params.frequency);
|
||||
gl.uniform1f(u.phase, 0.0);
|
||||
gl.uniform1f(u.threshold, params.threshold);
|
||||
gl.uniform3f(u.color, params.color[0], params.color[1], params.color[2]);
|
||||
gl.uniform1f(u.opacity, params.opacity);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineImageEffectorFx } from '../ImageEffector.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const shader = `#version 300 es
|
||||
precision mediump float;
|
||||
|
||||
in vec2 in_uv;
|
||||
uniform sampler2D in_texture;
|
||||
uniform vec2 in_resolution;
|
||||
uniform float u_r;
|
||||
uniform float u_g;
|
||||
uniform float u_b;
|
||||
out vec4 out_color;
|
||||
|
||||
void main() {
|
||||
vec4 in_color = texture(in_texture, in_uv);
|
||||
float r = in_color.r < u_r ? 0.0 : 1.0;
|
||||
float g = in_color.g < u_g ? 0.0 : 1.0;
|
||||
float b = in_color.b < u_b ? 0.0 : 1.0;
|
||||
out_color = vec4(r, g, b, in_color.a);
|
||||
}
|
||||
`;
|
||||
|
||||
export const FX_threshold = defineImageEffectorFx({
|
||||
id: 'threshold' as const,
|
||||
name: i18n.ts._imageEffector._fxs.threshold,
|
||||
shader,
|
||||
uniforms: ['r', 'g', 'b'] as const,
|
||||
params: {
|
||||
r: {
|
||||
type: 'number' as const,
|
||||
default: 0.5,
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
step: 0.01,
|
||||
},
|
||||
g: {
|
||||
type: 'number' as const,
|
||||
default: 0.5,
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
step: 0.01,
|
||||
},
|
||||
b: {
|
||||
type: 'number' as const,
|
||||
default: 0.5,
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
step: 0.01,
|
||||
},
|
||||
},
|
||||
main: ({ gl, u, params }) => {
|
||||
gl.uniform1f(u.r, params.r);
|
||||
gl.uniform1f(u.g, params.g);
|
||||
gl.uniform1f(u.b, params.b);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineImageEffectorFx } from '../ImageEffector.js';
|
||||
|
||||
const shader = `#version 300 es
|
||||
precision mediump float;
|
||||
|
||||
const float PI = 3.141592653589793;
|
||||
const float TWO_PI = 6.283185307179586;
|
||||
const float HALF_PI = 1.5707963267948966;
|
||||
|
||||
in vec2 in_uv;
|
||||
uniform sampler2D in_texture;
|
||||
uniform vec2 in_resolution;
|
||||
uniform sampler2D u_texture_watermark;
|
||||
uniform vec2 u_resolution_watermark;
|
||||
uniform float u_scale;
|
||||
uniform float u_angle;
|
||||
uniform float u_opacity;
|
||||
uniform bool u_repeat;
|
||||
uniform int u_alignX; // 0: left, 1: center, 2: right
|
||||
uniform int u_alignY; // 0: top, 1: center, 2: bottom
|
||||
uniform int u_fitMode; // 0: contain, 1: cover
|
||||
out vec4 out_color;
|
||||
|
||||
void main() {
|
||||
vec4 in_color = texture(in_texture, in_uv);
|
||||
float in_x_ratio = max(in_resolution.x / in_resolution.y, 1.0);
|
||||
float in_y_ratio = max(in_resolution.y / in_resolution.x, 1.0);
|
||||
|
||||
bool contain = u_fitMode == 0;
|
||||
|
||||
float x_ratio = u_resolution_watermark.x / in_resolution.x;
|
||||
float y_ratio = u_resolution_watermark.y / in_resolution.y;
|
||||
|
||||
float aspect_ratio = contain ?
|
||||
(min(x_ratio, y_ratio) / max(x_ratio, y_ratio)) :
|
||||
(max(x_ratio, y_ratio) / min(x_ratio, y_ratio));
|
||||
|
||||
float x_scale = contain ?
|
||||
(x_ratio > y_ratio ? 1.0 * u_scale : aspect_ratio * u_scale) :
|
||||
(x_ratio > y_ratio ? aspect_ratio * u_scale : 1.0 * u_scale);
|
||||
|
||||
float y_scale = contain ?
|
||||
(y_ratio > x_ratio ? 1.0 * u_scale : aspect_ratio * u_scale) :
|
||||
(y_ratio > x_ratio ? aspect_ratio * u_scale : 1.0 * u_scale);
|
||||
|
||||
float x_offset = u_alignX == 0 ? x_scale / 2.0 : u_alignX == 2 ? 1.0 - (x_scale / 2.0) : 0.5;
|
||||
float y_offset = u_alignY == 0 ? y_scale / 2.0 : u_alignY == 2 ? 1.0 - (y_scale / 2.0) : 0.5;
|
||||
|
||||
float angle = -(u_angle * PI);
|
||||
vec2 center = vec2(x_offset, y_offset);
|
||||
//vec2 centeredUv = (in_uv - center) * vec2(in_x_ratio, in_y_ratio);
|
||||
vec2 centeredUv = (in_uv - center);
|
||||
vec2 rotatedUV = vec2(
|
||||
centeredUv.x * cos(angle) - centeredUv.y * sin(angle),
|
||||
centeredUv.x * sin(angle) + centeredUv.y * cos(angle)
|
||||
) + center;
|
||||
|
||||
// trim
|
||||
if (!u_repeat) {
|
||||
bool isInside = rotatedUV.x > x_offset - (x_scale / 2.0) && rotatedUV.x < x_offset + (x_scale / 2.0) &&
|
||||
rotatedUV.y > y_offset - (y_scale / 2.0) && rotatedUV.y < y_offset + (y_scale / 2.0);
|
||||
if (!isInside) {
|
||||
out_color = in_color;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
vec4 watermark_color = texture(u_texture_watermark, vec2(
|
||||
(rotatedUV.x - (x_offset - (x_scale / 2.0))) / x_scale,
|
||||
(rotatedUV.y - (y_offset - (y_scale / 2.0))) / y_scale
|
||||
));
|
||||
|
||||
out_color.r = mix(in_color.r, watermark_color.r, u_opacity * watermark_color.a);
|
||||
out_color.g = mix(in_color.g, watermark_color.g, u_opacity * watermark_color.a);
|
||||
out_color.b = mix(in_color.b, watermark_color.b, u_opacity * watermark_color.a);
|
||||
out_color.a = in_color.a * (1.0 - u_opacity * watermark_color.a) + watermark_color.a * u_opacity;
|
||||
}
|
||||
`;
|
||||
|
||||
export const FX_watermarkPlacement = defineImageEffectorFx({
|
||||
id: 'watermarkPlacement' as const,
|
||||
name: '(internal)',
|
||||
shader,
|
||||
uniforms: ['texture_watermark', 'resolution_watermark', 'scale', 'angle', 'opacity', 'repeat', 'alignX', 'alignY', 'fitMode'] as const,
|
||||
params: {
|
||||
cover: {
|
||||
type: 'boolean' as const,
|
||||
default: false,
|
||||
},
|
||||
repeat: {
|
||||
type: 'boolean' as const,
|
||||
default: false,
|
||||
},
|
||||
scale: {
|
||||
type: 'number' as const,
|
||||
default: 0.3,
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
step: 0.01,
|
||||
},
|
||||
angle: {
|
||||
type: 'number' as const,
|
||||
default: 0,
|
||||
min: -1.0,
|
||||
max: 1.0,
|
||||
step: 0.01,
|
||||
},
|
||||
align: {
|
||||
type: 'align' as const,
|
||||
default: { x: 'right', y: 'bottom' },
|
||||
},
|
||||
opacity: {
|
||||
type: 'number' as const,
|
||||
default: 0.75,
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
step: 0.01,
|
||||
},
|
||||
watermark: {
|
||||
type: 'texture' as const,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
main: ({ gl, u, params, textures }) => {
|
||||
if (textures.watermark == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
gl.activeTexture(gl.TEXTURE1);
|
||||
gl.bindTexture(gl.TEXTURE_2D, textures.watermark.texture);
|
||||
gl.uniform1i(u.texture_watermark, 1);
|
||||
|
||||
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, params.angle);
|
||||
gl.uniform1i(u.repeat, params.repeat ? 1 : 0);
|
||||
gl.uniform1i(u.alignX, params.align.x === 'left' ? 0 : params.align.x === 'right' ? 2 : 1);
|
||||
gl.uniform1i(u.alignY, params.align.y === 'top' ? 0 : params.align.y === 'bottom' ? 2 : 1);
|
||||
gl.uniform1i(u.fitMode, params.cover ? 1 : 0);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineImageEffectorFx } from '../ImageEffector.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const shader = `#version 300 es
|
||||
precision mediump float;
|
||||
|
||||
in vec2 in_uv;
|
||||
uniform sampler2D in_texture;
|
||||
uniform vec2 in_resolution;
|
||||
uniform vec2 u_pos;
|
||||
uniform float u_frequency;
|
||||
uniform bool u_thresholdEnabled;
|
||||
uniform float u_threshold;
|
||||
uniform float u_maskSize;
|
||||
uniform bool u_black;
|
||||
out vec4 out_color;
|
||||
|
||||
void main() {
|
||||
vec4 in_color = texture(in_texture, in_uv);
|
||||
float angle = atan(-u_pos.y + (in_uv.y), -u_pos.x + (in_uv.x));
|
||||
float t = (1.0 + sin(angle * u_frequency)) / 2.0;
|
||||
if (u_thresholdEnabled) t = t < u_threshold ? 1.0 : 0.0;
|
||||
float d = distance(in_uv * vec2(2.0, 2.0), u_pos * vec2(2.0, 2.0));
|
||||
float mask = d < u_maskSize ? 0.0 : ((d - u_maskSize) * (1.0 + (u_maskSize * 2.0)));
|
||||
out_color = vec4(
|
||||
mix(in_color.r, u_black ? 0.0 : 1.0, t * mask),
|
||||
mix(in_color.g, u_black ? 0.0 : 1.0, t * mask),
|
||||
mix(in_color.b, u_black ? 0.0 : 1.0, t * mask),
|
||||
in_color.a
|
||||
);
|
||||
}
|
||||
`;
|
||||
|
||||
export const FX_zoomLines = defineImageEffectorFx({
|
||||
id: 'zoomLines' as const,
|
||||
name: i18n.ts._imageEffector._fxs.zoomLines,
|
||||
shader,
|
||||
uniforms: ['pos', 'frequency', 'thresholdEnabled', 'threshold', 'maskSize', 'black'] as const,
|
||||
params: {
|
||||
x: {
|
||||
type: 'number' as const,
|
||||
default: 0.0,
|
||||
min: -1.0,
|
||||
max: 1.0,
|
||||
step: 0.01,
|
||||
},
|
||||
y: {
|
||||
type: 'number' as const,
|
||||
default: 0.0,
|
||||
min: -1.0,
|
||||
max: 1.0,
|
||||
step: 0.01,
|
||||
},
|
||||
frequency: {
|
||||
type: 'number' as const,
|
||||
default: 30.0,
|
||||
min: 1.0,
|
||||
max: 200.0,
|
||||
step: 0.1,
|
||||
},
|
||||
thresholdEnabled: {
|
||||
type: 'boolean' as const,
|
||||
default: true,
|
||||
},
|
||||
threshold: {
|
||||
type: 'number' as const,
|
||||
default: 0.2,
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
step: 0.01,
|
||||
},
|
||||
maskSize: {
|
||||
type: 'number' as const,
|
||||
default: 0.5,
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
step: 0.01,
|
||||
},
|
||||
black: {
|
||||
type: 'boolean' as const,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
main: ({ gl, u, params }) => {
|
||||
gl.uniform2f(u.pos, (1.0 + params.x) / 2.0, (1.0 + params.y) / 2.0);
|
||||
gl.uniform1f(u.frequency, params.frequency);
|
||||
gl.uniform1i(u.thresholdEnabled, params.thresholdEnabled ? 1 : 0);
|
||||
gl.uniform1f(u.threshold, params.threshold);
|
||||
gl.uniform1f(u.maskSize, params.maskSize);
|
||||
gl.uniform1i(u.black, params.black ? 1 : 0);
|
||||
},
|
||||
});
|
||||
@@ -38,7 +38,7 @@ export class SnowfallEffect {
|
||||
`;
|
||||
|
||||
private FRAGMENT_SOURCE = `#version 300 es
|
||||
precision highp float;
|
||||
precision mediump float;
|
||||
|
||||
in vec4 v_color;
|
||||
in float v_rotation;
|
||||
|
||||
180
packages/frontend/src/utility/watermark.ts
Normal file
180
packages/frontend/src/utility/watermark.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { FX_watermarkPlacement } from './image-effector/fxs/watermarkPlacement.js';
|
||||
import { FX_stripe } from './image-effector/fxs/stripe.js';
|
||||
import { FX_polkadot } from './image-effector/fxs/polkadot.js';
|
||||
import { FX_checker } from './image-effector/fxs/checker.js';
|
||||
import type { ImageEffectorLayer } from '@/utility/image-effector/ImageEffector.js';
|
||||
import { ImageEffector } from '@/utility/image-effector/ImageEffector.js';
|
||||
|
||||
export type WatermarkPreset = {
|
||||
id: string;
|
||||
name: string;
|
||||
layers: ({
|
||||
id: string;
|
||||
type: 'text';
|
||||
text: string;
|
||||
repeat: boolean;
|
||||
scale: number;
|
||||
angle: number;
|
||||
align: { x: 'left' | 'center' | 'right'; y: 'top' | 'center' | 'bottom' };
|
||||
opacity: number;
|
||||
} | {
|
||||
id: string;
|
||||
type: 'image';
|
||||
imageUrl: string | null;
|
||||
imageId: string | null;
|
||||
cover: boolean;
|
||||
repeat: boolean;
|
||||
scale: number;
|
||||
angle: number;
|
||||
align: { x: 'left' | 'center' | 'right'; y: 'top' | 'center' | 'bottom' };
|
||||
opacity: number;
|
||||
} | {
|
||||
id: string;
|
||||
type: 'stripe';
|
||||
angle: number;
|
||||
frequency: number;
|
||||
threshold: number;
|
||||
color: [r: number, g: number, b: number];
|
||||
opacity: number;
|
||||
} | {
|
||||
id: string;
|
||||
type: 'polkadot';
|
||||
angle: number;
|
||||
scale: number;
|
||||
majorRadius: number;
|
||||
majorOpacity: number;
|
||||
minorDivisions: number;
|
||||
minorRadius: number;
|
||||
minorOpacity: number;
|
||||
color: [r: number, g: number, b: number];
|
||||
opacity: number;
|
||||
} | {
|
||||
id: string;
|
||||
type: 'checker';
|
||||
angle: number;
|
||||
scale: number;
|
||||
color: [r: number, g: number, b: number];
|
||||
opacity: number;
|
||||
})[];
|
||||
};
|
||||
|
||||
export class WatermarkRenderer {
|
||||
private effector: ImageEffector;
|
||||
private layers: WatermarkPreset['layers'] = [];
|
||||
|
||||
constructor(options: {
|
||||
canvas: HTMLCanvasElement,
|
||||
renderWidth: number,
|
||||
renderHeight: number,
|
||||
image: HTMLImageElement | ImageBitmap,
|
||||
}) {
|
||||
this.effector = new ImageEffector({
|
||||
canvas: options.canvas,
|
||||
renderWidth: options.renderWidth,
|
||||
renderHeight: options.renderHeight,
|
||||
image: options.image,
|
||||
fxs: [FX_watermarkPlacement, FX_stripe, FX_polkadot, FX_checker],
|
||||
});
|
||||
}
|
||||
|
||||
private makeImageEffectorLayers(): ImageEffectorLayer[] {
|
||||
return this.layers.map(layer => {
|
||||
if (layer.type === 'text') {
|
||||
return {
|
||||
fxId: 'watermarkPlacement',
|
||||
id: layer.id,
|
||||
params: {
|
||||
repeat: layer.repeat,
|
||||
scale: layer.scale,
|
||||
align: layer.align,
|
||||
angle: layer.angle,
|
||||
opacity: layer.opacity,
|
||||
cover: false,
|
||||
watermark: {
|
||||
type: 'text',
|
||||
text: layer.text,
|
||||
},
|
||||
},
|
||||
};
|
||||
} else if (layer.type === 'image') {
|
||||
return {
|
||||
fxId: 'watermarkPlacement',
|
||||
id: layer.id,
|
||||
params: {
|
||||
repeat: layer.repeat,
|
||||
scale: layer.scale,
|
||||
align: layer.align,
|
||||
angle: layer.angle,
|
||||
opacity: layer.opacity,
|
||||
cover: layer.cover,
|
||||
watermark: {
|
||||
type: 'url',
|
||||
url: layer.imageUrl,
|
||||
},
|
||||
},
|
||||
};
|
||||
} else if (layer.type === 'stripe') {
|
||||
return {
|
||||
fxId: 'stripe',
|
||||
id: layer.id,
|
||||
params: {
|
||||
angle: layer.angle,
|
||||
frequency: layer.frequency,
|
||||
threshold: layer.threshold,
|
||||
color: layer.color,
|
||||
opacity: layer.opacity,
|
||||
},
|
||||
};
|
||||
} else if (layer.type === 'polkadot') {
|
||||
return {
|
||||
fxId: 'polkadot',
|
||||
id: layer.id,
|
||||
params: {
|
||||
angle: layer.angle,
|
||||
scale: layer.scale,
|
||||
majorRadius: layer.majorRadius,
|
||||
majorOpacity: layer.majorOpacity,
|
||||
minorDivisions: layer.minorDivisions,
|
||||
minorRadius: layer.minorRadius,
|
||||
minorOpacity: layer.minorOpacity,
|
||||
color: layer.color,
|
||||
opacity: layer.opacity,
|
||||
},
|
||||
};
|
||||
} else if (layer.type === 'checker') {
|
||||
return {
|
||||
fxId: 'checker',
|
||||
id: layer.id,
|
||||
params: {
|
||||
angle: layer.angle,
|
||||
scale: layer.scale,
|
||||
color: layer.color,
|
||||
opacity: layer.opacity,
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async setLayers(layers: WatermarkPreset['layers']) {
|
||||
this.layers = layers;
|
||||
await this.effector.setLayers(this.makeImageEffectorLayers());
|
||||
this.render();
|
||||
}
|
||||
|
||||
public render(): void {
|
||||
this.effector.render();
|
||||
}
|
||||
|
||||
/*
|
||||
* disposeCanvas = true だとloseContextを呼ぶため、コンストラクタで渡されたcanvasも再利用不可になるので注意
|
||||
*/
|
||||
public destroy(disposeCanvas = true): void {
|
||||
this.effector.destroy(disposeCanvas);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user