From 7b0c41410e5eeaa48e2bca7d3084a8cd4105e16a Mon Sep 17 00:00:00 2001 From: Lukas Hollaender Date: Tue, 24 Sep 2024 13:15:00 +0200 Subject: [PATCH] don't use requestAnimationFrame in Canvg.render(); instead render as soon as all images and fonts are loaded --- src/Canvg.ts | 18 +++++++++--------- src/Document/Document.ts | 10 ++++++++++ src/Document/ImageElement.ts | 5 +++-- src/SVGFontLoader.ts | 8 +++++++- src/Screen.ts | 31 ++++++++++++++++++++++++++++--- 5 files changed, 57 insertions(+), 15 deletions(-) diff --git a/src/Canvg.ts b/src/Canvg.ts index 787e57305..7025b3f58 100644 --- a/src/Canvg.ts +++ b/src/Canvg.ts @@ -153,17 +153,17 @@ export default class Canvg { * Render only first frame, ignoring animations and mouse. * @param options - Rendering options. */ - async render(options: IScreenStartOptions = {}) { - this.start({ - enableRedraw: true, - ignoreAnimation: true, - ignoreMouse: true, + render(options: Omit = {}) { + const { + documentElement, + screen, + options: baseOptions + } = this; + + return screen.render(documentElement, { + ...baseOptions, ...options }); - - await this.ready(); - - this.stop(); } /** diff --git a/src/Document/Document.ts b/src/Document/Document.ts index 45f9fbb62..80ae3a86a 100644 --- a/src/Document/Document.ts +++ b/src/Document/Document.ts @@ -115,6 +115,8 @@ export default class Document { this.screen.wait(this.isImagesLoaded.bind(this)); this.screen.wait(this.isFontsLoaded.bind(this)); + this.screen.waitPromise(this.loadImages.bind(this)); + this.screen.waitPromise(this.loadFonts.bind(this)); } private bindCreateImage(createImage: CreateImage, anonymousCrossOrigin?: boolean) { @@ -178,6 +180,14 @@ export default class Document { return this.fonts.every(_ => _.loaded); } + async loadImages(): Promise { + await Promise.all(this.images.map(_ => _.loadingPromise)); + } + + async loadFonts(): Promise { + await Promise.all(this.fonts.map(_ => _.loadingPromise)); + } + createDocumentElement(document: DOMDocument) { const documentElement = this.createElement(document.documentElement); diff --git a/src/Document/ImageElement.ts b/src/Document/ImageElement.ts index 16240d3b0..947dfcc9b 100644 --- a/src/Document/ImageElement.ts +++ b/src/Document/ImageElement.ts @@ -11,6 +11,7 @@ const dataUriRegex = /^\s*data:(([^/,;]+\/[^/,;]+)(?:;([^,;=]+=[^,;=]+))?)?(?:;( export default class ImageElement extends RenderedElement { type = 'image'; loaded = false; + loadingPromise: Promise; protected readonly isSvg: boolean; protected image: CanvasImageSource | string; @@ -32,9 +33,9 @@ export default class ImageElement extends RenderedElement { document.images.push(this); if (!isSvg) { - void this.loadImage(href); + this.loadingPromise = this.loadImage(href); } else { - void this.loadSvg(href); + this.loadingPromise = this.loadSvg(href); } this.isSvg = isSvg; diff --git a/src/SVGFontLoader.ts b/src/SVGFontLoader.ts index 9bd5f3215..5e795c0af 100644 --- a/src/SVGFontLoader.ts +++ b/src/SVGFontLoader.ts @@ -2,6 +2,7 @@ import Document from './Document'; export default class SVGFontLoader { loaded = false; + loadingPromise: Promise; constructor( private readonly document: Document @@ -9,7 +10,12 @@ export default class SVGFontLoader { document.fonts.push(this); } - async load(fontFamily: string, url: string) { + load(fontFamily: string, url: string) { + this.loadingPromise = this.loadCore(url, fontFamily); + return this.loadingPromise; + } + + private async loadCore(fontFamily: string, url: string) { try { const { document diff --git a/src/Screen.ts b/src/Screen.ts index 01b84a797..1ce51e3f1 100644 --- a/src/Screen.ts +++ b/src/Screen.ts @@ -108,6 +108,7 @@ export default class Screen { private readyPromise: Promise; private resolveReady: () => void; private waits: (() => boolean)[] = []; + private loadingPromises: (() => Promise)[] = []; private frameDuration = 0; private isReadyLock = false; private isFirstRender = true; @@ -128,6 +129,10 @@ export default class Screen { this.waits.push(checker); } + waitPromise(promise: () => Promise) { + this.loadingPromises.push(promise); + } + ready() { // eslint-disable-next-line @typescript-eslint/no-misused-promises if (!this.readyPromise) { @@ -157,6 +162,11 @@ export default class Screen { return isReadyLock; } + private async finishLoading(): Promise { + await Promise.all(this.loadingPromises.map(_ => _())); + this.loadingPromises = []; + } + setDefaults(ctx: RenderingContext2D) { // initial values and defaults ctx.strokeStyle = 'rgba(0,0,0,0)'; @@ -283,6 +293,21 @@ export default class Screen { ctx.translate(-minX, -minY); } + async render( + element: Element, + { + ignoreDimensions = false, + ignoreClear = false, + scaleWidth, + scaleHeight, + offsetX, + offsetY + }: IScreenStartOptions = {} + ) { + await this.finishLoading(); + this.renderFrame(element, ignoreDimensions, ignoreClear, scaleWidth, scaleHeight, offsetX, offsetY); + } + start( element: Element, { @@ -310,7 +335,7 @@ export default class Screen { }); if (this.isReady()) { - this.render( + this.renderFrame( element, ignoreDimensions, ignoreClear, @@ -339,7 +364,7 @@ export default class Screen { ignoreAnimation, forceRedraw )) { - this.render( + this.renderFrame( element, ignoreDimensions, ignoreClear, @@ -407,7 +432,7 @@ export default class Screen { return false; } - private render( + private renderFrame( element: Element, ignoreDimensions: boolean, ignoreClear: boolean,