From 7037d5dff2a2e1bf2fca34ee8042da5e853dd94d Mon Sep 17 00:00:00 2001 From: David Schulte Date: Mon, 8 Jul 2024 20:42:14 +0200 Subject: [PATCH] Only clearing the used area on the canvas, because clearing the whole area every frame is slow. --- src/pgsRendererInternal.ts | 13 ++++++- src/utils/rect.ts | 79 ++++++++++++++++++++++++++++++++++++++ tests/rect.test.ts | 63 ++++++++++++++++++++++++++++++ 3 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 src/utils/rect.ts create mode 100644 tests/rect.test.ts diff --git a/src/pgsRendererInternal.ts b/src/pgsRendererInternal.ts index 98c1b2e..37e7741 100644 --- a/src/pgsRendererInternal.ts +++ b/src/pgsRendererInternal.ts @@ -6,6 +6,7 @@ import {CombinedBinaryReader} from "./utils/combinedBinaryReader"; import {PaletteDefinitionSegment} from "./pgs/paletteDefinitionSegment"; import {ObjectDefinitionSegment} from "./pgs/objectDefinitionSegment"; import {WindowDefinition} from "./pgs/windowDefinitionSegment"; +import {Rect} from "./utils/rect"; /** * This handles the low-level PGS loading and rendering. This renderer can operate inside the web worker without being @@ -58,6 +59,9 @@ export class PgsRendererInternal { private canvas?: OffscreenCanvas | HTMLCanvasElement; private context?: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D; + // We keep track of the dirty area on the canvas. Clearing the whole canvas is slow when only a small area was used. + private readonly dirtyArea = new Rect(); + /** * Sets the canvas to render to. * @param canvas The canvas to render to. @@ -96,7 +100,10 @@ export class PgsRendererInternal { // Clear the canvas on invalid indices. It is possible to seek to a position before the first subtitle while // a later subtile is on screen. This subtitle must be clear, even there is no valid new subtitle data. // Ignoring the render would keep the previous subtitle on screen. - this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); + if (!this.dirtyArea.empty) { + this.context.clearRect(this.dirtyArea.x, this.dirtyArea.y, this.dirtyArea.width, this.dirtyArea.height); + this.dirtyArea.reset(); + } if (index < 0 || index >= this.displaySets.length) { return; } @@ -149,6 +156,10 @@ export class PgsRendererInternal { const pixelData = this.getPixelDataFromComposition(compositionObject, palette, ctxObjects); if (pixelData) { this.context.drawImage(pixelData, window.horizontalPosition, window.verticalPosition); + + // Mark this area as dirty. + this.dirtyArea.union(window.horizontalPosition, window.verticalPosition, + pixelData.width, pixelData.height); } } } diff --git a/src/utils/rect.ts b/src/utils/rect.ts new file mode 100644 index 0000000..0660c11 --- /dev/null +++ b/src/utils/rect.ts @@ -0,0 +1,79 @@ +/** + * A simple rectangular class. + */ +export class Rect { + /** + * Gets if the rect is still empty and doesn't contain any area. + */ + public empty: boolean = true; + + /** + * Gets the x coordinate of the rectangular area if not empty. + */ + public x: number = 0; + + /** + * Gets the y coordinate of the rectangular area if not empty. + */ + public y: number = 0; + + /** + * Gets the width of the rectangular area if not empty. + */ + public width: number = 0; + + /** + * Gets the height of the rectangular area if not empty. + */ + public height: number = 0; + + /** + * Clears the rectangular. + */ + public reset() { + this.empty = true; + this.x = 0; + this.y = 0; + this.width = 0; + this.height = 0; + } + + /** + * Grows this rectangular area to include the given area. + * @param x The x coordinate of the new area. + * @param y The y coordinate of the new area. + * @param width The width of the new area. Negative values are not supported. + * @param height The height of the new area. Negative values are not supported. + */ + public set(x: number, y: number, width: number = 0, height: number = 0) { + this.empty = false; + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } + + /** + * Grows this rectangular area to include the given area. + * @param x The x coordinate of the new area to include. + * @param y The y coordinate of the new area to include. + * @param width The width of the new area to include. Negative values are not supported. + * @param height The height of the new area to include. Negative values are not supported. + */ + public union(x: number, y: number, width: number = 0, height: number = 0) { + if (this.empty) { + // First sub-rect added + this.empty = false; + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } else { + // Grow the rectangular area to fit the new sub-rect + if (x < this.x) { this.width += this.x - x; this.x = x; } + if (y < this.y) { this.height += this.y - y; this.y = y; } + if (x + width > this.x + this.width) { this.width = x + width - this.x; } + if (y + height > this.y + this.height) { this.height = y + height - this.y; } + } + } +} diff --git a/tests/rect.test.ts b/tests/rect.test.ts new file mode 100644 index 0000000..566d3cb --- /dev/null +++ b/tests/rect.test.ts @@ -0,0 +1,63 @@ +import {Rect} from "../src/utils/rect"; + +test('reset rect', () => { + const rect = new Rect(); + rect.set(1, 1, 2, 2); + expect(rect.empty).toBe(false); + rect.reset(); + expect(rect.empty).toBe(true); +}); + +test('grow rect in positive x direction', () => { + const rect = new Rect(); + + rect.set(1, 1, 2, 2); + rect.union(5, 1, 2, 2); + + expect(rect.empty).toBe(false); + expect(rect.x).toBe(1); + expect(rect.y).toBe(1); + expect(rect.width).toBe(6); + expect(rect.height).toBe(2); +}); + + +test('grow rect in negative x direction', () => { + const rect = new Rect(); + + rect.set(5, 1, 2, 2); + rect.union(1, 1, 2, 2); + + expect(rect.empty).toBe(false); + expect(rect.x).toBe(1); + expect(rect.y).toBe(1); + expect(rect.width).toBe(6); + expect(rect.height).toBe(2); +}); + +test('grow rect in positive y direction', () => { + const rect = new Rect(); + + rect.set(1, 1, 2, 2); + rect.union(1, 5, 2, 2); + + expect(rect.empty).toBe(false); + expect(rect.x).toBe(1); + expect(rect.y).toBe(1); + expect(rect.width).toBe(2); + expect(rect.height).toBe(6); +}); + + +test('grow rect in negative y direction', () => { + const rect = new Rect(); + + rect.set(1, 5, 2, 2); + rect.union(1, 1, 2, 2); + + expect(rect.empty).toBe(false); + expect(rect.x).toBe(1); + expect(rect.y).toBe(1); + expect(rect.width).toBe(2); + expect(rect.height).toBe(6); +});