1
0
mirror of https://github.com/misskey-dev/misskey.git synced 2026-05-14 16:56:00 +02:00

fix/refactor(frontend): 画像編集機能の修正・型強化 (#16156)

* enhance: refine uploadFile

* fix: missing locale

* refactor: harden types

* refactor: シェーダーファイルをlazy-loadingできるように

* fix(frontend): omit console.log in production environment

* fix: glslのバージョン表記は最初の行になければならない

* fix: シェーダーの読み込みが完了してからレンダリングを行うように

* fix merge failure

* fix: ウォーターマークのプリセットがない場合にdividerが2重に表示される問題を修正

* fix: アップローダーダイアログの機能設定でウォーターマークが無効な場合でもデフォルトのプリセットが適用されてしまう問題を修正

* fix lint

* Revert "fix: シェーダーの読み込みが完了してからレンダリングを行うように"

This reverts commit e06f37a7d4.

* Revert "fix: glslのバージョン表記は最初の行になければならない"

This reverts commit afcc37d886.

* Revert "refactor: シェーダーファイルをlazy-loadingできるように"

This reverts commit a1ab2fa38c.

* fix: ウォーターマークのFX定義を分ける

* Update packages/frontend/src/components/MkWatermarkEditorDialog.vue

* Update packages/frontend/src/components/MkWatermarkEditorDialog.vue

* Update packages/frontend/src/components/MkWatermarkEditorDialog.vue

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
This commit is contained in:
かっこかり
2025-06-04 16:22:09 +09:00
committed by GitHub
parent e3b57a118d
commit b43dfa260b
23 changed files with 196 additions and 69 deletions

View File

@@ -16,6 +16,7 @@ import { instance } from '@/instance.js';
import { globalEvents } from '@/events.js';
import { getProxiedImageUrl } from '@/utility/media-proxy.js';
import { genId } from '@/utility/id.js';
import type { UploaderDialogFeatures } from '@/components/MkUploaderDialog.vue';
type UploadReturnType = {
filePromise: Promise<Misskey.entities.DriveFile>;
@@ -155,6 +156,7 @@ export function uploadFile(file: File | Blob, options: {
export function chooseFileFromPcAndUpload(
options: {
multiple?: boolean;
features?: UploaderDialogFeatures;
folderId?: string | null;
} = {},
): Promise<Misskey.entities.DriveFile[]> {
@@ -163,6 +165,7 @@ export function chooseFileFromPcAndUpload(
if (files.length === 0) return;
os.launchUploader(files, {
folderId: options.folderId,
features: options.features,
}).then(driveFiles => {
res(driveFiles);
});
@@ -194,7 +197,7 @@ export function chooseFileFromUrl(): Promise<Misskey.entities.DriveFile> {
type: 'url',
placeholder: i18n.ts.uploadFromUrlDescription,
}).then(({ canceled, result: url }) => {
if (canceled) return;
if (canceled || url == null) return;
const marker = genId();
@@ -221,7 +224,7 @@ export function chooseFileFromUrl(): Promise<Misskey.entities.DriveFile> {
});
}
function select(anchorElement: HTMLElement | EventTarget | null, label: string | null, multiple: boolean): Promise<Misskey.entities.DriveFile[]> {
function select(anchorElement: HTMLElement | EventTarget | null, label: string | null, multiple: boolean, features?: UploaderDialogFeatures): Promise<Misskey.entities.DriveFile[]> {
return new Promise((res, rej) => {
os.popupMenu([label ? {
text: label,
@@ -229,7 +232,7 @@ function select(anchorElement: HTMLElement | EventTarget | null, label: string |
} : undefined, {
text: i18n.ts.upload,
icon: 'ti ti-upload',
action: () => chooseFileFromPcAndUpload({ multiple }).then(files => res(files)),
action: () => chooseFileFromPcAndUpload({ multiple, features }).then(files => res(files)),
}, {
text: i18n.ts.fromDrive,
icon: 'ti ti-cloud',
@@ -242,12 +245,19 @@ function select(anchorElement: HTMLElement | EventTarget | null, label: string |
});
}
export function selectFile(anchorElement: HTMLElement | EventTarget | null, label: string | null = null): Promise<Misskey.entities.DriveFile> {
return select(anchorElement, label, false).then(files => files[0]);
}
type SelectFileOptions<M extends boolean> = {
anchorElement: HTMLElement | EventTarget | null;
multiple: M;
label?: string | null;
features?: UploaderDialogFeatures;
};
export function selectFiles(anchorElement: HTMLElement | EventTarget | null, label: string | null = null): Promise<Misskey.entities.DriveFile[]> {
return select(anchorElement, label, true);
export async function selectFile<
M extends boolean,
MR extends M extends true ? Misskey.entities.DriveFile[] : Misskey.entities.DriveFile
>(opts: SelectFileOptions<M>): Promise<MR> {
const files = await select(opts.anchorElement, opts.label ?? null, opts.multiple ?? false, opts.features);
return opts.multiple ? (files as MR) : (files[0]! as MR);
}
export async function createCroppedImageDriveFileFromImageDriveFile(imageDriveFile: Misskey.entities.DriveFile, options: {

View File

@@ -57,7 +57,7 @@ function getValue<T extends keyof ParamTypeToPrimitive>(params: Record<string, a
return params[k];
}
export class ImageEffector {
export class ImageEffector<IEX extends ReadonlyArray<ImageEffectorFx<any, any, any>>> {
private gl: WebGL2RenderingContext;
private canvas: HTMLCanvasElement | null = null;
private renderTextureProgram: WebGLProgram;
@@ -70,7 +70,7 @@ export class ImageEffector {
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 fxs: [...IEX];
private paramTextures: Map<string, { texture: WebGLTexture; width: number; height: number; }> = new Map();
constructor(options: {
@@ -78,7 +78,7 @@ export class ImageEffector {
renderWidth: number;
renderHeight: number;
image: ImageData | ImageBitmap | HTMLImageElement | HTMLCanvasElement;
fxs: ImageEffectorFx[];
fxs: [...IEX];
}) {
this.canvas = options.canvas;
this.renderWidth = options.renderWidth;
@@ -230,7 +230,7 @@ export class ImageEffector {
gl: gl,
program: shaderProgram,
params: Object.fromEntries(
Object.entries(fx.params).map(([key, param]) => {
Object.entries(fx.params as ImageEffectorFxParamDefs).map(([key, param]) => {
return [key, layer.params[key] ?? param.default];
}),
),
@@ -238,7 +238,7 @@ export class ImageEffector {
width: this.renderWidth,
height: this.renderHeight,
textures: Object.fromEntries(
Object.entries(fx.params).map(([k, v]) => {
Object.entries(fx.params as ImageEffectorFxParamDefs).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];
@@ -329,7 +329,7 @@ export class ImageEffector {
unused.delete(textureKey);
if (this.paramTextures.has(textureKey)) continue;
console.log(`Baking texture of <${textureKey}>...`);
if (_DEV_) 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;
@@ -339,7 +339,7 @@ export class ImageEffector {
}
for (const k of unused) {
console.log(`Dispose unused texture <${k}>...`);
if (_DEV_) console.log(`Dispose unused texture <${k}>...`);
this.gl.deleteTexture(this.paramTextures.get(k)!.texture);
this.paramTextures.delete(k);
}

View File

@@ -3,13 +3,20 @@
* 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 { FX_watermarkPlacement } from '@/utility/image-effector/fxs/watermarkPlacement.js';
import { FX_stripe } from '@/utility/image-effector/fxs/stripe.js';
import { FX_polkadot } from '@/utility/image-effector/fxs/polkadot.js';
import { FX_checker } from '@/utility/image-effector/fxs/checker.js';
import type { ImageEffectorFx, ImageEffectorLayer } from '@/utility/image-effector/ImageEffector.js';
import { ImageEffector } from '@/utility/image-effector/ImageEffector.js';
const WATERMARK_FXS = [
FX_watermarkPlacement,
FX_stripe,
FX_polkadot,
FX_checker,
] as const satisfies ImageEffectorFx<string, any>[];
export type WatermarkPreset = {
id: string;
name: string;
@@ -64,7 +71,7 @@ export type WatermarkPreset = {
};
export class WatermarkRenderer {
private effector: ImageEffector;
private effector: ImageEffector<typeof WATERMARK_FXS>;
private layers: WatermarkPreset['layers'] = [];
constructor(options: {
@@ -78,7 +85,7 @@ export class WatermarkRenderer {
renderWidth: options.renderWidth,
renderHeight: options.renderHeight,
image: options.image,
fxs: [FX_watermarkPlacement, FX_stripe, FX_polkadot, FX_checker],
fxs: WATERMARK_FXS,
});
}
@@ -157,6 +164,8 @@ export class WatermarkRenderer {
opacity: layer.opacity,
},
};
} else {
throw new Error(`Unknown layer type`);
}
});
}