mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
fix preview perf issues
This commit is contained in:
@@ -69,6 +69,7 @@ function RenderTreeController() {
|
|||||||
duration,
|
duration,
|
||||||
canvasSize: { width, height },
|
canvasSize: { width, height },
|
||||||
background: activeProject.settings.background,
|
background: activeProject.settings.background,
|
||||||
|
isPreview: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
editor.renderer.setRenderTree({ renderTree });
|
editor.renderer.setRenderTree({ renderTree });
|
||||||
|
|||||||
@@ -3,26 +3,71 @@ import { VisualNode, type VisualNodeParams } from "./visual-node";
|
|||||||
|
|
||||||
export interface ImageNodeParams extends VisualNodeParams {
|
export interface ImageNodeParams extends VisualNodeParams {
|
||||||
url: string;
|
url: string;
|
||||||
|
maxSourceSize?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ImageNode extends VisualNode<ImageNodeParams> {
|
interface CachedImageSource {
|
||||||
private image?: HTMLImageElement;
|
source: HTMLImageElement | OffscreenCanvas;
|
||||||
private readyPromise: Promise<void>;
|
width: number;
|
||||||
|
height: number;
|
||||||
constructor(params: ImageNodeParams) {
|
|
||||||
super(params);
|
|
||||||
this.readyPromise = this.load();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async load() {
|
const imageSourceCache = new Map<string, Promise<CachedImageSource>>();
|
||||||
|
|
||||||
|
function loadImageSource(
|
||||||
|
url: string,
|
||||||
|
maxSourceSize?: number,
|
||||||
|
): Promise<CachedImageSource> {
|
||||||
|
const cacheKey = `${url}::${maxSourceSize ?? "full"}`;
|
||||||
|
|
||||||
|
const cached = imageSourceCache.get(cacheKey);
|
||||||
|
if (cached) return cached;
|
||||||
|
|
||||||
|
const promise = (async (): Promise<CachedImageSource> => {
|
||||||
const image = new Image();
|
const image = new Image();
|
||||||
this.image = image;
|
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
image.onload = () => resolve();
|
image.onload = () => resolve();
|
||||||
image.onerror = () => reject(new Error("Image load failed"));
|
image.onerror = () => reject(new Error("Image load failed"));
|
||||||
image.src = this.params.url;
|
image.src = url;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const naturalWidth = image.naturalWidth;
|
||||||
|
const naturalHeight = image.naturalHeight;
|
||||||
|
const exceedsLimit =
|
||||||
|
maxSourceSize &&
|
||||||
|
(naturalWidth > maxSourceSize || naturalHeight > maxSourceSize);
|
||||||
|
|
||||||
|
if (exceedsLimit) {
|
||||||
|
const scale = Math.min(
|
||||||
|
maxSourceSize / naturalWidth,
|
||||||
|
maxSourceSize / naturalHeight,
|
||||||
|
);
|
||||||
|
const scaledWidth = Math.round(naturalWidth * scale);
|
||||||
|
const scaledHeight = Math.round(naturalHeight * scale);
|
||||||
|
|
||||||
|
const offscreen = new OffscreenCanvas(scaledWidth, scaledHeight);
|
||||||
|
const ctx = offscreen.getContext("2d");
|
||||||
|
|
||||||
|
if (ctx) {
|
||||||
|
ctx.drawImage(image, 0, 0, scaledWidth, scaledHeight);
|
||||||
|
return { source: offscreen, width: scaledWidth, height: scaledHeight };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { source: image, width: naturalWidth, height: naturalHeight };
|
||||||
|
})();
|
||||||
|
|
||||||
|
imageSourceCache.set(cacheKey, promise);
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ImageNode extends VisualNode<ImageNodeParams> {
|
||||||
|
private cachedSource: Promise<CachedImageSource>;
|
||||||
|
|
||||||
|
constructor(params: ImageNodeParams) {
|
||||||
|
super(params);
|
||||||
|
this.cachedSource = loadImageSource(params.url, params.maxSourceSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
async render({ renderer, time }: { renderer: CanvasRenderer; time: number }) {
|
async render({ renderer, time }: { renderer: CanvasRenderer; time: number }) {
|
||||||
@@ -32,20 +77,13 @@ export class ImageNode extends VisualNode<ImageNodeParams> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.readyPromise;
|
const { source, width, height } = await this.cachedSource;
|
||||||
|
|
||||||
if (!this.image) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mediaW = this.image.naturalWidth || renderer.width;
|
|
||||||
const mediaH = this.image.naturalHeight || renderer.height;
|
|
||||||
|
|
||||||
this.renderVisual({
|
this.renderVisual({
|
||||||
renderer,
|
renderer,
|
||||||
source: this.image,
|
source,
|
||||||
sourceWidth: mediaW,
|
sourceWidth: width || renderer.width,
|
||||||
sourceHeight: mediaH,
|
sourceHeight: height || renderer.height,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,29 +6,46 @@ export interface StickerNodeParams extends VisualNodeParams {
|
|||||||
stickerId: string;
|
stickerId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class StickerNode extends VisualNode<StickerNodeParams> {
|
interface CachedStickerSource {
|
||||||
private image?: HTMLImageElement;
|
source: HTMLImageElement;
|
||||||
private readyPromise: Promise<void>;
|
width: number;
|
||||||
|
height: number;
|
||||||
constructor(params: StickerNodeParams) {
|
|
||||||
super(params);
|
|
||||||
this.readyPromise = this.load();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async load() {
|
const stickerSourceCache = new Map<string, Promise<CachedStickerSource>>();
|
||||||
const image = new Image();
|
|
||||||
this.image = image;
|
function loadStickerSource(stickerId: string): Promise<CachedStickerSource> {
|
||||||
|
const cached = stickerSourceCache.get(stickerId);
|
||||||
|
if (cached) return cached;
|
||||||
|
|
||||||
|
const promise = (async (): Promise<CachedStickerSource> => {
|
||||||
const url = resolveStickerId({
|
const url = resolveStickerId({
|
||||||
stickerId: this.params.stickerId,
|
stickerId,
|
||||||
options: { width: 200, height: 200 },
|
options: { width: 200, height: 200 },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const image = new Image();
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
image.onload = () => resolve();
|
image.onload = () => resolve();
|
||||||
image.onerror = () =>
|
image.onerror = () =>
|
||||||
reject(new Error(`Failed to load sticker: ${this.params.stickerId}`));
|
reject(new Error(`Failed to load sticker: ${stickerId}`));
|
||||||
image.src = url;
|
image.src = url;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return { source: image, width: 200, height: 200 };
|
||||||
|
})();
|
||||||
|
|
||||||
|
stickerSourceCache.set(stickerId, promise);
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class StickerNode extends VisualNode<StickerNodeParams> {
|
||||||
|
private cachedSource: Promise<CachedStickerSource>;
|
||||||
|
|
||||||
|
constructor(params: StickerNodeParams) {
|
||||||
|
super(params);
|
||||||
|
this.cachedSource = loadStickerSource(params.stickerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async render({ renderer, time }: { renderer: CanvasRenderer; time: number }) {
|
async render({ renderer, time }: { renderer: CanvasRenderer; time: number }) {
|
||||||
@@ -38,17 +55,13 @@ export class StickerNode extends VisualNode<StickerNodeParams> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.readyPromise;
|
const { source, width, height } = await this.cachedSource;
|
||||||
|
|
||||||
if (!this.image) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.renderVisual({
|
this.renderVisual({
|
||||||
renderer,
|
renderer,
|
||||||
source: this.image,
|
source,
|
||||||
sourceWidth: 200,
|
sourceWidth: width,
|
||||||
sourceHeight: 200,
|
sourceHeight: height,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,12 +11,15 @@ import type { TBackground, TCanvasSize } from "@/types/project";
|
|||||||
import { DEFAULT_BLUR_INTENSITY } from "@/constants/project-constants";
|
import { DEFAULT_BLUR_INTENSITY } from "@/constants/project-constants";
|
||||||
import { isMainTrack } from "@/lib/timeline";
|
import { isMainTrack } from "@/lib/timeline";
|
||||||
|
|
||||||
|
const PREVIEW_MAX_IMAGE_SIZE = 2048;
|
||||||
|
|
||||||
export type BuildSceneParams = {
|
export type BuildSceneParams = {
|
||||||
canvasSize: TCanvasSize;
|
canvasSize: TCanvasSize;
|
||||||
tracks: TimelineTrack[];
|
tracks: TimelineTrack[];
|
||||||
mediaAssets: MediaAsset[];
|
mediaAssets: MediaAsset[];
|
||||||
duration: number;
|
duration: number;
|
||||||
background: TBackground;
|
background: TBackground;
|
||||||
|
isPreview?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function buildScene(params: BuildSceneParams) {
|
export function buildScene(params: BuildSceneParams) {
|
||||||
@@ -81,6 +84,9 @@ export function buildScene(params: BuildSceneParams) {
|
|||||||
transform: element.transform,
|
transform: element.transform,
|
||||||
opacity: element.opacity,
|
opacity: element.opacity,
|
||||||
blendMode: element.blendMode,
|
blendMode: element.blendMode,
|
||||||
|
...(params.isPreview && {
|
||||||
|
maxSourceSize: PREVIEW_MAX_IMAGE_SIZE,
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user