fix preview perf issues
This commit is contained in:
parent
dbfecbba59
commit
baa5f33407
|
|
@ -69,6 +69,7 @@ function RenderTreeController() {
|
|||
duration,
|
||||
canvasSize: { width, height },
|
||||
background: activeProject.settings.background,
|
||||
isPreview: true,
|
||||
});
|
||||
|
||||
editor.renderer.setRenderTree({ renderTree });
|
||||
|
|
|
|||
|
|
@ -3,26 +3,71 @@ import { VisualNode, type VisualNodeParams } from "./visual-node";
|
|||
|
||||
export interface ImageNodeParams extends VisualNodeParams {
|
||||
url: string;
|
||||
maxSourceSize?: number;
|
||||
}
|
||||
|
||||
export class ImageNode extends VisualNode<ImageNodeParams> {
|
||||
private image?: HTMLImageElement;
|
||||
private readyPromise: Promise<void>;
|
||||
interface CachedImageSource {
|
||||
source: HTMLImageElement | OffscreenCanvas;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
constructor(params: ImageNodeParams) {
|
||||
super(params);
|
||||
this.readyPromise = this.load();
|
||||
}
|
||||
const imageSourceCache = new Map<string, Promise<CachedImageSource>>();
|
||||
|
||||
private async load() {
|
||||
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();
|
||||
this.image = image;
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
image.onload = () => resolve();
|
||||
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 }) {
|
||||
|
|
@ -32,20 +77,13 @@ export class ImageNode extends VisualNode<ImageNodeParams> {
|
|||
return;
|
||||
}
|
||||
|
||||
await this.readyPromise;
|
||||
|
||||
if (!this.image) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mediaW = this.image.naturalWidth || renderer.width;
|
||||
const mediaH = this.image.naturalHeight || renderer.height;
|
||||
const { source, width, height } = await this.cachedSource;
|
||||
|
||||
this.renderVisual({
|
||||
renderer,
|
||||
source: this.image,
|
||||
sourceWidth: mediaW,
|
||||
sourceHeight: mediaH,
|
||||
source,
|
||||
sourceWidth: width || renderer.width,
|
||||
sourceHeight: height || renderer.height,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,29 +6,46 @@ export interface StickerNodeParams extends VisualNodeParams {
|
|||
stickerId: string;
|
||||
}
|
||||
|
||||
export class StickerNode extends VisualNode<StickerNodeParams> {
|
||||
private image?: HTMLImageElement;
|
||||
private readyPromise: Promise<void>;
|
||||
interface CachedStickerSource {
|
||||
source: HTMLImageElement;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
constructor(params: StickerNodeParams) {
|
||||
super(params);
|
||||
this.readyPromise = this.load();
|
||||
}
|
||||
const stickerSourceCache = new Map<string, Promise<CachedStickerSource>>();
|
||||
|
||||
private async load() {
|
||||
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({
|
||||
stickerId: this.params.stickerId,
|
||||
stickerId,
|
||||
options: { width: 200, height: 200 },
|
||||
});
|
||||
|
||||
const image = new Image();
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
image.onload = () => resolve();
|
||||
image.onerror = () =>
|
||||
reject(new Error(`Failed to load sticker: ${this.params.stickerId}`));
|
||||
reject(new Error(`Failed to load sticker: ${stickerId}`));
|
||||
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 }) {
|
||||
|
|
@ -38,17 +55,13 @@ export class StickerNode extends VisualNode<StickerNodeParams> {
|
|||
return;
|
||||
}
|
||||
|
||||
await this.readyPromise;
|
||||
|
||||
if (!this.image) {
|
||||
return;
|
||||
}
|
||||
const { source, width, height } = await this.cachedSource;
|
||||
|
||||
this.renderVisual({
|
||||
renderer,
|
||||
source: this.image,
|
||||
sourceWidth: 200,
|
||||
sourceHeight: 200,
|
||||
source,
|
||||
sourceWidth: width,
|
||||
sourceHeight: height,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,12 +11,15 @@ import type { TBackground, TCanvasSize } from "@/types/project";
|
|||
import { DEFAULT_BLUR_INTENSITY } from "@/constants/project-constants";
|
||||
import { isMainTrack } from "@/lib/timeline";
|
||||
|
||||
const PREVIEW_MAX_IMAGE_SIZE = 2048;
|
||||
|
||||
export type BuildSceneParams = {
|
||||
canvasSize: TCanvasSize;
|
||||
tracks: TimelineTrack[];
|
||||
mediaAssets: MediaAsset[];
|
||||
duration: number;
|
||||
background: TBackground;
|
||||
isPreview?: boolean;
|
||||
};
|
||||
|
||||
export function buildScene(params: BuildSceneParams) {
|
||||
|
|
@ -81,6 +84,9 @@ export function buildScene(params: BuildSceneParams) {
|
|||
transform: element.transform,
|
||||
opacity: element.opacity,
|
||||
blendMode: element.blendMode,
|
||||
...(params.isPreview && {
|
||||
maxSourceSize: PREVIEW_MAX_IMAGE_SIZE,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue