This commit is contained in:
syuilo 2025-05-28 12:54:48 +09:00
parent 33486ebdf2
commit 09eb631fdc
7 changed files with 46 additions and 45 deletions

View File

@ -96,7 +96,7 @@ import { isWebpSupported } from '@/utility/isWebpSupported.js';
import { uploadFile, UploadAbortedError } from '@/utility/drive.js'; import { uploadFile, UploadAbortedError } from '@/utility/drive.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { ensureSignin } from '@/i.js'; import { ensureSignin } from '@/i.js';
import { Watermarker } from '@/utility/watermarker.js'; import { ImageEffector } from '@/utility/ImageEffector.js';
const $i = ensureSignin(); const $i = ensureSignin();
@ -152,7 +152,7 @@ const items = ref<{
uploaded: Misskey.entities.DriveFile | null; uploaded: Misskey.entities.DriveFile | null;
uploadFailed: boolean; uploadFailed: boolean;
aborted: boolean; aborted: boolean;
compressionLevel: 0 | 1 | 2 | 3; compressionLevel: number;
compressedSize?: number | null; compressedSize?: number | null;
preprocessedFile?: Blob | null; preprocessedFile?: Blob | null;
file: File; file: File;
@ -486,11 +486,11 @@ async function preprocess(item: (typeof items)['value'][number]): Promise<void>
const preset = prefer.s.watermarkPresets.find(p => p.id === item.watermarkPresetId); const preset = prefer.s.watermarkPresets.find(p => p.id === item.watermarkPresetId);
if (needsWatermark && preset != null) { if (needsWatermark && preset != null) {
const canvas = window.document.createElement('canvas'); const canvas = window.document.createElement('canvas');
const renderer = new Watermarker({ const renderer = new ImageEffector({
canvas: canvas, canvas: canvas,
width: img.width, width: img.width,
height: img.height, height: img.height,
preset: preset, layers: preset.layers,
originalImage: img, originalImage: img,
}); });

View File

@ -87,9 +87,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<script setup lang="ts"> <script setup lang="ts">
import { ref, useTemplateRef, watch, onMounted, onUnmounted } from 'vue'; import { ref, useTemplateRef, watch, onMounted, onUnmounted } from 'vue';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import type { WatermarkerLayer, WatermarkPreset } from '@/utility/watermarker.js'; import type { ImageEffectorLayer } from '@/utility/ImageEffector.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { Watermarker } from '@/utility/watermarker.js'; import { ImageEffector } from '@/utility/ImageEffector.js';
import MkSelect from '@/components/MkSelect.vue'; import MkSelect from '@/components/MkSelect.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue'; import MkInput from '@/components/MkInput.vue';
@ -102,7 +102,7 @@ import { selectFile } from '@/utility/drive.js';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
import { prefer } from '@/preferences.js'; import { prefer } from '@/preferences.js';
const layer = defineModel<WatermarkerLayer>('layer', { required: true }); const layer = defineModel<ImageEffectorLayer>('layer', { required: true });
const driveFile = ref(); const driveFile = ref();
const driveFileError = ref(false); const driveFileError = ref(false);

View File

@ -45,9 +45,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<script setup lang="ts"> <script setup lang="ts">
import { ref, useTemplateRef, watch, onMounted, onUnmounted, reactive } from 'vue'; import { ref, useTemplateRef, watch, onMounted, onUnmounted, reactive } from 'vue';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import type { WatermarkPreset } from '@/utility/watermarker.js'; import type { WatermarkPreset } from '@/preferences/def.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { Watermarker } from '@/utility/watermarker.js'; import { ImageEffector } from '@/utility/ImageEffector.js';
import MkModalWindow from '@/components/MkModalWindow.vue'; import MkModalWindow from '@/components/MkModalWindow.vue';
import MkSelect from '@/components/MkSelect.vue'; import MkSelect from '@/components/MkSelect.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
@ -121,7 +121,7 @@ watch(type, () => {
watch(preset, async (newValue, oldValue) => { watch(preset, async (newValue, oldValue) => {
if (renderer != null) { if (renderer != null) {
renderer.updatePreset(preset); renderer.updateLayers(preset.layers);
} }
}, { deep: true }); }, { deep: true });
@ -148,25 +148,25 @@ watch(sampleImageType, async () => {
} }
}); });
let renderer: Watermarker | null = null; let renderer: ImageEffector | null = null;
async function initRenderer() { async function initRenderer() {
if (canvasEl.value == null) return; if (canvasEl.value == null) return;
if (sampleImageType.value === '3_2') { if (sampleImageType.value === '3_2') {
renderer = new Watermarker({ renderer = new ImageEffector({
canvas: canvasEl.value, canvas: canvasEl.value,
width: 1500, width: 1500,
height: 1000, height: 1000,
preset: preset, layers: preset.layers,
originalImage: sampleImage_3_2, originalImage: sampleImage_3_2,
}); });
} else if (sampleImageType.value === '2_3') { } else if (sampleImageType.value === '2_3') {
renderer = new Watermarker({ renderer = new ImageEffector({
canvas: canvasEl.value, canvas: canvasEl.value,
width: 1000, width: 1000,
height: 1500, height: 1500,
preset: preset, layers: preset.layers,
originalImage: sampleImage_2_3, originalImage: sampleImage_2_3,
}); });
} }

View File

@ -22,13 +22,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { defineAsyncComponent, onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue'; import { defineAsyncComponent, onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue';
import type { WatermarkPreset } from '@/utility/watermarker.js'; import type { WatermarkPreset } from '@/preferences/def.js';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { deepClone } from '@/utility/clone.js'; import { deepClone } from '@/utility/clone.js';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import { Watermarker } from '@/utility/watermarker.js'; import { ImageEffector } from '@/utility/ImageEffector.js';
const props = defineProps<{ const props = defineProps<{
preset: WatermarkPreset; preset: WatermarkPreset;
@ -64,18 +64,18 @@ const canvasEl = useTemplateRef('canvasEl');
const sampleImage = new Image(); const sampleImage = new Image();
sampleImage.src = '/client-assets/sample/3-2.jpg'; sampleImage.src = '/client-assets/sample/3-2.jpg';
let renderer: Watermarker | null = null; let renderer: ImageEffector | null = null;
onMounted(() => { onMounted(() => {
sampleImage.onload = async () => { sampleImage.onload = async () => {
watch(canvasEl, async () => { watch(canvasEl, async () => {
if (canvasEl.value == null) return; if (canvasEl.value == null) return;
renderer = new Watermarker({ renderer = new ImageEffector({
canvas: canvasEl.value, canvas: canvasEl.value,
width: 1500, width: 1500,
height: 1000, height: 1000,
preset: props.preset, layers: props.preset.layers,
originalImage: sampleImage, originalImage: sampleImage,
}); });
@ -95,7 +95,7 @@ onUnmounted(() => {
watch(() => props.preset, async () => { watch(() => props.preset, async () => {
if (renderer != null) { if (renderer != null) {
renderer.updatePreset(props.preset); renderer.updateLayers(props.preset.layers);
await renderer.bakeTextures(); await renderer.bakeTextures();
renderer.render(); renderer.render();
} }

View File

@ -149,7 +149,7 @@ import { computed, defineAsyncComponent, ref } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
import XWatermarkItem from './drive.WatermarkItem.vue'; import XWatermarkItem from './drive.WatermarkItem.vue';
import type { WatermarkPreset } from '@/utility/watermarker.js'; import type { WatermarkPreset } from '@/preferences/def.js';
import FormLink from '@/components/form/link.vue'; import FormLink from '@/components/form/link.vue';
import MkSwitch from '@/components/MkSwitch.vue'; import MkSwitch from '@/components/MkSwitch.vue';
import MkSelect from '@/components/MkSelect.vue'; import MkSelect from '@/components/MkSelect.vue';

View File

@ -11,7 +11,7 @@ import type { Plugin } from '@/plugin.js';
import type { DeviceKind } from '@/utility/device-kind.js'; import type { DeviceKind } from '@/utility/device-kind.js';
import type { DeckProfile } from '@/deck.js'; import type { DeckProfile } from '@/deck.js';
import type { PreferencesDefinition } from './manager.js'; import type { PreferencesDefinition } from './manager.js';
import type { WatermarkPreset } from '@/utility/watermarker.js'; import type { ImageEffectorLayer } from '@/utility/ImageEffector.js';
import { DEFAULT_DEVICE_KIND } from '@/utility/device-kind.js'; import { DEFAULT_DEVICE_KIND } from '@/utility/device-kind.js';
/** サウンド設定 */ /** サウンド設定 */
@ -30,6 +30,12 @@ export type SoundStore = {
volume: number; volume: number;
}; };
export type WatermarkPreset = {
id: string;
name: string;
layers: ImageEffectorLayer[];
};
// NOTE: デフォルト値は他の設定の状態に依存してはならない(依存していた場合、ユーザーがその設定項目単体で「初期値にリセット」した場合不具合の原因になる) // NOTE: デフォルト値は他の設定の状態に依存してはならない(依存していた場合、ユーザーがその設定項目単体で「初期値にリセット」した場合不具合の原因になる)
export const PREF_DEF = { export const PREF_DEF = {

View File

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
const IMAGE_ADD_SHADER = `#version 300 es const IMAGE_PLACEMENT_SHADER = `#version 300 es
precision highp float; precision highp float;
in vec2 in_uv; in vec2 in_uv;
@ -51,13 +51,7 @@ void main() {
} }
`; `;
export type WatermarkPreset = { type ImageEffectorTextLayer = {
id: string;
name: string;
layers: WatermarkerLayer[];
};
type WatermarkerTextLayer = {
id: string; id: string;
type: 'text'; type: 'text';
text: string; text: string;
@ -68,7 +62,7 @@ type WatermarkerTextLayer = {
opacity: number; opacity: number;
}; };
type WatermarkerImageLayer = { type ImageEffectorImageLayer = {
id: string; id: string;
type: 'image'; type: 'image';
imageUrl: string | null; imageUrl: string | null;
@ -80,9 +74,9 @@ type WatermarkerImageLayer = {
opacity: number; opacity: number;
}; };
export type WatermarkerLayer = WatermarkerTextLayer | WatermarkerImageLayer; export type ImageEffectorLayer = ImageEffectorTextLayer | ImageEffectorImageLayer;
export class Watermarker { export class ImageEffector {
private canvas: HTMLCanvasElement | null = null; private canvas: HTMLCanvasElement | null = null;
private gl: WebGL2RenderingContext | null = null; private gl: WebGL2RenderingContext | null = null;
private renderTextureProgram!: WebGLProgram; private renderTextureProgram!: WebGLProgram;
@ -90,7 +84,7 @@ export class Watermarker {
private renderWidth!: number; private renderWidth!: number;
private renderHeight!: number; private renderHeight!: number;
private originalImage: ImageData | ImageBitmap | HTMLImageElement | HTMLCanvasElement; private originalImage: ImageData | ImageBitmap | HTMLImageElement | HTMLCanvasElement;
private preset: WatermarkPreset; private layers: ImageEffectorLayer[];
private originalImageTexture: WebGLTexture; private originalImageTexture: WebGLTexture;
private resultTexture: WebGLTexture; private resultTexture: WebGLTexture;
private resultFrameBuffer: WebGLFramebuffer; private resultFrameBuffer: WebGLFramebuffer;
@ -102,7 +96,7 @@ export class Watermarker {
width: number; width: number;
height: number; height: number;
originalImage: ImageData | ImageBitmap | HTMLImageElement | HTMLCanvasElement; originalImage: ImageData | ImageBitmap | HTMLImageElement | HTMLCanvasElement;
preset: WatermarkPreset; layers: ImageEffectorLayer[];
}) { }) {
this.canvas = options.canvas; this.canvas = options.canvas;
this.canvas.width = options.width; this.canvas.width = options.width;
@ -110,7 +104,7 @@ export class Watermarker {
this.renderWidth = options.width; this.renderWidth = options.width;
this.renderHeight = options.height; this.renderHeight = options.height;
this.originalImage = options.originalImage; this.originalImage = options.originalImage;
this.preset = options.preset; this.layers = options.layers;
this.texturesKey = this.calcTexturesKey(); this.texturesKey = this.calcTexturesKey();
this.gl = this.canvas.getContext('webgl2', { this.gl = this.canvas.getContext('webgl2', {
@ -180,7 +174,7 @@ export class Watermarker {
} }
private calcTexturesKey() { private calcTexturesKey() {
return this.preset.layers.map(layer => { return this.layers.map(layer => {
if (layer.type === 'image') { if (layer.type === 'image') {
return layer.imageId; return layer.imageId;
} else if (layer.type === 'text') { } else if (layer.type === 'text') {
@ -224,10 +218,11 @@ export class Watermarker {
this.disposeBakedTextures(); this.disposeBakedTextures();
for (const layer of this.preset.layers) { for (const layer of this.layers) {
if (layer.type === 'image') { if (layer.type === 'image') {
const image = await new Promise<HTMLImageElement>((resolve, reject) => { const image = await new Promise<HTMLImageElement>((resolve, reject) => {
const img = new Image(); const img = new Image();
img.crossOrigin = 'use-credentials';
img.onload = () => resolve(img); img.onload = () => resolve(img);
img.onerror = reject; img.onerror = reject;
img.src = layer.imageUrl; img.src = layer.imageUrl;
@ -332,7 +327,7 @@ export class Watermarker {
return shaderProgram; return shaderProgram;
} }
private renderTextOrImageLayer(layer: WatermarkerTextLayer | WatermarkerImageLayer) { private renderTextOrImageLayer(layer: ImageEffectorTextLayer | ImageEffectorImageLayer) {
const gl = this.gl; const gl = this.gl;
if (gl == null) { if (gl == null) {
throw new Error('gl is not initialized'); throw new Error('gl is not initialized');
@ -351,7 +346,7 @@ export class Watermarker {
in_uv = (position + 1.0) / 2.0; in_uv = (position + 1.0) / 2.0;
gl_Position = vec4(position, 0.0, 1.0); gl_Position = vec4(position, 0.0, 1.0);
} }
`, IMAGE_ADD_SHADER); `, IMAGE_PLACEMENT_SHADER);
gl.useProgram(shaderProgram); gl.useProgram(shaderProgram);
@ -392,7 +387,7 @@ export class Watermarker {
gl.drawArrays(gl.TRIANGLES, 0, 6); gl.drawArrays(gl.TRIANGLES, 0, 6);
} }
private renderLayer(layer: WatermarkerLayer) { private renderLayer(layer: ImageEffectorLayer) {
if (layer.type === 'image') { if (layer.type === 'image') {
this.renderTextOrImageLayer(layer); this.renderTextOrImageLayer(layer);
} else if (layer.type === 'text') { } else if (layer.type === 'text') {
@ -435,7 +430,7 @@ export class Watermarker {
// -------------------- // --------------------
for (const layer of this.preset.layers) { for (const layer of this.layers) {
this.renderLayer(layer); this.renderLayer(layer);
} }
@ -450,8 +445,8 @@ export class Watermarker {
gl.drawArrays(gl.TRIANGLES, 0, 6); gl.drawArrays(gl.TRIANGLES, 0, 6);
} }
public async updatePreset(preset: WatermarkPreset) { public async updateLayers(layers: ImageEffectorLayer[]) {
this.preset = preset; this.layers = layers;
const newTexturesKey = this.calcTexturesKey(); const newTexturesKey = this.calcTexturesKey();
if (newTexturesKey !== this.texturesKey) { if (newTexturesKey !== this.texturesKey) {