/* * 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); } }