Skip to content

Commit

Permalink
Implement bounds handling for Canvas Text
Browse files Browse the repository at this point in the history
  • Loading branch information
wouterlucas authored and frank-weindel committed Jul 5, 2024
1 parent a53008f commit aff3a4a
Show file tree
Hide file tree
Showing 3 changed files with 250 additions and 14 deletions.
1 change: 1 addition & 0 deletions examples/tests/text-baseline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ function generateBaselineTest(

const baselineNode = renderer.createTextNode({
...nodeProps,
parent: renderer.root,
});
const dimensions = await waitForLoadedDimensions(baselineNode);

Expand Down
191 changes: 191 additions & 0 deletions examples/tests/viewport-events-canvas.ts
Original file line number Diff line number Diff line change
@@ -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;
}
};
}
72 changes: 58 additions & 14 deletions src/core/text-rendering/renderers/CanvasTextRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
intersectRect,
type Bound,
intersectBound,
boundsOverlap,
getNormalizedRgbaComponents,
getNormalizedAlphaComponent,
type BoundWithValid,
Expand Down Expand Up @@ -198,11 +199,17 @@ export class CanvasTextRenderer extends TextRenderer<CanvasTextRendererState> {
},
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;
Expand Down Expand Up @@ -340,10 +347,27 @@ export class CanvasTextRenderer extends TextRenderer<CanvasTextRendererState> {
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;
}
Expand Down Expand Up @@ -676,9 +700,7 @@ export class CanvasTextRenderer extends TextRenderer<CanvasTextRendererState> {

// Invalidate renderWindow because the renderInfo changed
state.renderWindow = undefined;

const renderInfo = state.lightning2TextRenderer.calculateRenderInfo();
return renderInfo;
return state.renderInfo;
}

getAndCalculateVisibleWindow(state: CanvasTextRendererState): BoundWithValid {
Expand Down Expand Up @@ -756,11 +778,9 @@ export class CanvasTextRenderer extends TextRenderer<CanvasTextRendererState> {
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,
Expand Down Expand Up @@ -902,19 +922,43 @@ export class CanvasTextRenderer extends TextRenderer<CanvasTextRendererState> {
// }
}

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;
Expand Down Expand Up @@ -951,7 +995,7 @@ export class CanvasTextRenderer extends TextRenderer<CanvasTextRendererState> {

/**
* 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.
Expand Down

0 comments on commit aff3a4a

Please sign in to comment.