From aff3a4a9bcf3aeaaf575b25393b894e50d808ef7 Mon Sep 17 00:00:00 2001 From: Wouter lucas van Boesschoten Date: Wed, 22 May 2024 10:57:28 +0200 Subject: [PATCH] Implement bounds handling for Canvas Text --- examples/tests/text-baseline.ts | 1 + examples/tests/viewport-events-canvas.ts | 191 ++++++++++++++++++ .../renderers/CanvasTextRenderer.ts | 72 +++++-- 3 files changed, 250 insertions(+), 14 deletions(-) create mode 100644 examples/tests/viewport-events-canvas.ts diff --git a/examples/tests/text-baseline.ts b/examples/tests/text-baseline.ts index 6bb0aaa9..366a1c9d 100644 --- a/examples/tests/text-baseline.ts +++ b/examples/tests/text-baseline.ts @@ -76,6 +76,7 @@ function generateBaselineTest( const baselineNode = renderer.createTextNode({ ...nodeProps, + parent: renderer.root, }); const dimensions = await waitForLoadedDimensions(baselineNode); diff --git a/examples/tests/viewport-events-canvas.ts b/examples/tests/viewport-events-canvas.ts new file mode 100644 index 00000000..3502c566 --- /dev/null +++ b/examples/tests/viewport-events-canvas.ts @@ -0,0 +1,191 @@ +import type { IAnimationController } from '../../dist/exports/main-api.js'; +import type { ExampleSettings } from '../common/ExampleSettings.js'; +import test from './alpha-blending.js'; + +export default async function ({ renderer, testRoot }: ExampleSettings) { + const instructionText = renderer.createTextNode({ + text: 'Press space to start animation, arrow keys to move, enter to reset', + fontSize: 30, + x: 10, + y: 960, + fontFamily: 'Ubuntu-ssdf', + parent: testRoot, + }); + + const redStatus = renderer.createTextNode({ + text: 'Red Status: ', + fontSize: 30, + x: 10, + y: 50, + fontFamily: 'Ubuntu-ssdf', + parent: testRoot, + }); + + const blueStatus = renderer.createTextNode({ + text: 'Blue Status: ', + fontSize: 30, + x: 10, + y: 10, + fontFamily: 'Ubuntu-ssdf', + parent: testRoot, + }); + + const boundaryRect = renderer.createNode({ + x: 1920 / 2 - (1920 * 0.75) / 2, + y: 1080 / 2 - (1080 * 0.75) / 2, + width: 1440, + height: 810, + color: 0x000000ff, + clipping: true, + parent: testRoot, + }); + + const redText = renderer.createTextNode({ + x: 500, + y: 305, + alpha: 1, + width: 200, + height: 200, + color: 0xff0000ff, + pivot: 0, + text: 'red', + fontSize: 80, + fontFamily: 'sans-serif', + parent: boundaryRect, + }); + + redText.on('outOfBounds', () => { + console.log('red text out of bounds'); + redStatus.text = 'Red Status: text out of bounds'; + redStatus.color = 0xff0000ff; + }); + + redText.on('inViewport', () => { + console.log('red text in view port'); + redStatus.text = 'Red Status: text in view port'; + redStatus.color = 0x00ff00ff; + }); + + redText.on('inBounds', () => { + console.log('red text inside render bounds'); + redStatus.text = 'Red Status: text in bounds'; + redStatus.color = 0xffff00ff; + }); + + const blueText = renderer.createTextNode({ + x: 1920 / 2 - 200, + y: 100, + alpha: 1, + width: 200, + height: 200, + color: 0x0000ffff, + pivot: 0, + text: 'blue', + fontSize: 80, + fontFamily: 'sans-serif', + parent: testRoot, + }); + + blueText.on('outOfBounds', () => { + console.log('blue text ouf ot bounds'); + blueStatus.text = 'Blue Status: blue text out of bounds'; + blueStatus.color = 0xff0000ff; + }); + + blueText.on('inViewport', () => { + console.log('blue text in view port'); + blueStatus.text = 'Blue Status: blue text in view port'; + blueStatus.color = 0x00ff00ff; + }); + + blueText.on('inBounds', () => { + console.log('blue text inside render bounds'); + blueStatus.text = 'Blue Status: blue text in bounds'; + blueStatus.color = 0xffff00ff; + }); + + let runAnimation = false; + const animate = async () => { + redText + .animate( + { + x: -500, + }, + { + duration: 4000, + }, + ) + .start(); + + await blueText + .animate( + { + x: -1200, + }, + { + duration: 4000, + }, + ) + .start() + .waitUntilStopped(); + + redText.x = 1920 + 400; + blueText.x = 1920 + 400; + + redText + .animate( + { + x: 520, + }, + { + duration: 4000, + }, + ) + .start(); + + await blueText + .animate( + { + x: 1920 / 2 - 200, + }, + { + duration: 4000, + }, + ) + .start() + .waitUntilStopped(); + + if (runAnimation) { + // eslint-disable-next-line @typescript-eslint/no-misused-promises + setTimeout(animate, 2000); + } + }; + + const moveModifier = 10; + window.onkeydown = (e) => { + if (e.key === ' ') { + runAnimation = !runAnimation; + + if (runAnimation) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + animate(); + } + } + + if (e.key === 'ArrowRight') { + redText.x += moveModifier; + blueText.x += moveModifier; + } + + if (e.key === 'ArrowLeft') { + redText.x -= moveModifier; + blueText.x -= moveModifier; + } + + if (e.key === 'Enter') { + runAnimation = false; + redText.x = 520; + blueText.x = 1920 / 2 - 200; + } + }; +} diff --git a/src/core/text-rendering/renderers/CanvasTextRenderer.ts b/src/core/text-rendering/renderers/CanvasTextRenderer.ts index cad52b9d..31b1e8f2 100644 --- a/src/core/text-rendering/renderers/CanvasTextRenderer.ts +++ b/src/core/text-rendering/renderers/CanvasTextRenderer.ts @@ -25,6 +25,7 @@ import { intersectRect, type Bound, intersectBound, + boundsOverlap, getNormalizedRgbaComponents, getNormalizedAlphaComponent, type BoundWithValid, @@ -198,11 +199,17 @@ export class CanvasTextRenderer extends TextRenderer { }, x: (state, value) => { state.props.x = value; - this.invalidateVisibleWindowCache(state); + + if (this.isValidOnScreen(state) === true) { + this.invalidateVisibleWindowCache(state); + } }, y: (state, value) => { state.props.y = value; - this.invalidateVisibleWindowCache(state); + + if (this.isValidOnScreen(state) === true) { + this.invalidateVisibleWindowCache(state); + } }, contain: (state, value) => { state.props.contain = value; @@ -340,10 +347,27 @@ export class CanvasTextRenderer extends TextRenderer { override updateState(state: CanvasTextRendererState): void { // On the first update call we need to set the status to loading if (state.status === 'initialState') { - this.setStatus(state, 'loading'); + // check if we're on screen + if (this.isValidOnScreen(state) === true) { + this.setStatus(state, 'loading'); + } + } + + // If the state is not renderable, we don't want to keep the texture + if (state.isRenderable === false && state.status === 'loaded') { + return this.destroyState(state); } - if (state.status === 'loaded') { + if ( + state.isRenderable === false && + (state.status === 'initialState' || state.status === 'destroyed') + ) { + // If the state is not renderable and we're in the initial or destroyed state + // we don't need to do anything else. + return; + } + + if (state.status === 'loaded' && state.visibleWindow.valid === true) { // If we're loaded, we don't need to do anything return; } @@ -676,9 +700,7 @@ export class CanvasTextRenderer extends TextRenderer { // Invalidate renderWindow because the renderInfo changed state.renderWindow = undefined; - - const renderInfo = state.lightning2TextRenderer.calculateRenderInfo(); - return renderInfo; + return state.renderInfo; } getAndCalculateVisibleWindow(state: CanvasTextRendererState): BoundWithValid { @@ -756,11 +778,9 @@ export class CanvasTextRenderer extends TextRenderer { const { stage } = this; const { canvasPages, textW = 0, textH = 0, renderWindow } = state; - if (!canvasPages || !renderWindow) return; const { x, y, scrollY, contain, width, height /*, debug*/ } = state.props; - const elementRect = { x: x, y: y, @@ -902,19 +922,43 @@ export class CanvasTextRenderer extends TextRenderer { // } } + isValidOnScreen(state: CanvasTextRendererState): boolean { + // if we dont have a valid render window, we can't be on screen + if (!state.visibleWindow.valid === false) { + return false; + } + + const { x, y, width, height, contain } = state.props; + const elementBounds = createBound( + x, + y, + contain !== 'none' ? x + width : Infinity, + contain === 'both' ? y + height : Infinity, + tmpElementBounds, + ); + + const isPossiblyOnScreen = boundsOverlap( + elementBounds, + this.rendererBounds, + ); + + return isPossiblyOnScreen; + } + override setIsRenderable( state: CanvasTextRendererState, renderable: boolean, ): void { super.setIsRenderable(state, renderable); - // Set state object owner from any canvas page textures - state.canvasPages?.forEach((pageInfo) => { - pageInfo.texture?.setRenderableOwner(state, renderable); - }); + this.updateState(state); } override destroyState(state: CanvasTextRendererState): void { + if (state.status === 'destroyed') { + return; + } super.destroyState(state); + // Remove state object owner from any canvas page textures state.canvasPages?.forEach((pageInfo) => { const { texture } = pageInfo; @@ -951,7 +995,7 @@ export class CanvasTextRenderer extends TextRenderer { /** * Invalidate the layout cache stored in the state. This will cause the text - * to be re-layed out on the next update. + * to be re-rendered on the next update. * * @remarks * This also invalidates the visible window cache.