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 commite06f37a7d4. * Revert "fix: glslのバージョン表記は最初の行になければならない" This reverts commitafcc37d886. * Revert "refactor: シェーダーファイルをlazy-loadingできるように" This reverts commita1ab2fa38c. * 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:
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user