Skip to content

Commit be06ab7

Browse files
Improve Canvas Text Renderer Performance (#319)
- Simplify/improve canvas renderer performance by utilizing child nodes and ImageTexture factory. - All canvas text is now laid out when created, but not actually rasterized until the text becomes renderable (inbounds, positive alpha, etc) - Canvas textures are now properly able to be cleaned up when they become unrenderable.
2 parents e695432 + 09f4705 commit be06ab7

File tree

14 files changed

+638
-566
lines changed

14 files changed

+638
-566
lines changed

examples/tests/text-baseline.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ function generateBaselineTest(
7676

7777
const baselineNode = renderer.createTextNode({
7878
...nodeProps,
79+
parent: renderer.root,
7980
});
8081
const dimensions = await waitForLoadedDimensions(baselineNode);
8182

examples/tests/text-canvas.ts

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/*
2+
* If not stated otherwise in this file or this component's LICENSE file the
3+
* following copyright and licenses apply:
4+
*
5+
* Copyright 2024 Comcast Cable Communications Management, LLC.
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the License);
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
20+
import type { ExampleSettings } from '../common/ExampleSettings.js';
21+
22+
const Colors = {
23+
Black: 0x000000ff,
24+
Red: 0xff0000ff,
25+
Green: 0x00ff00ff,
26+
Blue: 0x0000ffff,
27+
Magenta: 0xff00ffff,
28+
Gray: 0x7f7f7fff,
29+
White: 0xffffffff,
30+
};
31+
32+
const randomIntBetween = (from: number, to: number) =>
33+
Math.floor(Math.random() * (to - from + 1) + from);
34+
35+
/**
36+
* Tests that Single-Channel Signed Distance Field (SSDF) fonts are rendered
37+
* correctly.
38+
*
39+
* Text that is thinner than the certified snapshot may indicate that the
40+
* SSDF font atlas texture was premultiplied before being uploaded to the GPU.
41+
*
42+
* @param settings
43+
* @returns
44+
*/
45+
export default async function test(settings: ExampleSettings) {
46+
const { renderer, testRoot } = settings;
47+
48+
// Set a smaller snapshot area
49+
// testRoot.width = 200;
50+
// testRoot.height = 200;
51+
// testRoot.color = 0xffffffff;
52+
53+
const nodes: any[] = [];
54+
55+
const renderNode = (t: string) => {
56+
const node = renderer.createTextNode({
57+
x: Math.random() * 1900,
58+
y: Math.random() * 1080,
59+
text: 'CANVAS ' + t,
60+
fontFamily: 'sans-serif',
61+
parent: testRoot,
62+
fontSize: 80,
63+
});
64+
65+
nodes.push(node);
66+
67+
// pick random color from Colors
68+
node.color =
69+
Object.values(Colors)[
70+
randomIntBetween(0, Object.keys(Colors).length - 1)
71+
] || 0xff0000ff;
72+
};
73+
74+
const spawn = (amount = 100) => {
75+
for (let i = 0; i < amount; i++) {
76+
renderNode(i.toString());
77+
}
78+
};
79+
80+
const despawn = (amount = 100) => {
81+
for (let i = 0; i < amount; i++) {
82+
const node = nodes.pop();
83+
node.destroy();
84+
}
85+
};
86+
87+
const move = () => {
88+
for (let i = 0; i < nodes.length; i++) {
89+
const node = nodes[i];
90+
node.x = randomIntBetween(0, 1600);
91+
node.y = randomIntBetween(0, 880);
92+
}
93+
};
94+
95+
const newColor = () => {
96+
for (let i = 0; i < nodes.length; i++) {
97+
const node = nodes[i];
98+
node.color =
99+
Object.values(Colors)[
100+
randomIntBetween(0, Object.keys(Colors).length - 1)
101+
] || 0x000000ff;
102+
}
103+
};
104+
105+
let animating = false;
106+
const animate = () => {
107+
animating = !animating;
108+
109+
const animateNode = (node: any) => {
110+
nodes.forEach((node) => {
111+
node
112+
.animate(
113+
{
114+
x: randomIntBetween(20, 1740),
115+
y: randomIntBetween(20, 900),
116+
rotation: Math.random() * Math.PI,
117+
},
118+
{
119+
duration: 3000,
120+
easing: 'ease-out',
121+
},
122+
)
123+
.start();
124+
});
125+
};
126+
127+
const animateRun = () => {
128+
if (animating) {
129+
for (let i = 0; i < nodes.length; i++) {
130+
animateNode(nodes[i]);
131+
}
132+
setTimeout(animateRun, 3050);
133+
}
134+
};
135+
136+
animateRun();
137+
};
138+
139+
window.addEventListener('keydown', (event) => {
140+
if (event.key === 'ArrowUp') {
141+
spawn();
142+
} else if (event.key === 'ArrowDown') {
143+
despawn();
144+
} else if (event.key === 'ArrowLeft') {
145+
move();
146+
} else if (event.key === 'ArrowRight') {
147+
move();
148+
} else if (event.key === '1') {
149+
newColor();
150+
} else if (event.key === ' ') {
151+
animate();
152+
}
153+
});
154+
155+
spawn();
156+
}
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
/*
2+
* If not stated otherwise in this file or this component's LICENSE file the
3+
* following copyright and licenses apply:
4+
*
5+
* Copyright 2024 Comcast Cable Communications Management, LLC.
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the License);
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
20+
import type { ExampleSettings } from '../common/ExampleSettings.js';
21+
22+
export default async function ({ renderer, testRoot }: ExampleSettings) {
23+
const instructionText = renderer.createTextNode({
24+
text: 'Press space to start animation, arrow keys to move, enter to reset',
25+
fontSize: 30,
26+
x: 10,
27+
y: 960,
28+
fontFamily: 'Ubuntu-ssdf',
29+
parent: testRoot,
30+
});
31+
32+
const redStatus = renderer.createTextNode({
33+
text: 'Red Status: ',
34+
fontSize: 30,
35+
x: 10,
36+
y: 50,
37+
fontFamily: 'Ubuntu-ssdf',
38+
parent: testRoot,
39+
});
40+
41+
const blueStatus = renderer.createTextNode({
42+
text: 'Blue Status: ',
43+
fontSize: 30,
44+
x: 10,
45+
y: 10,
46+
fontFamily: 'Ubuntu-ssdf',
47+
parent: testRoot,
48+
});
49+
50+
const boundaryRect = renderer.createNode({
51+
x: 1920 / 2 - (1920 * 0.75) / 2,
52+
y: 1080 / 2 - (1080 * 0.75) / 2,
53+
width: 1440,
54+
height: 810,
55+
color: 0x000000ff,
56+
clipping: true,
57+
parent: testRoot,
58+
});
59+
60+
const redText = renderer.createTextNode({
61+
x: 500,
62+
y: 305,
63+
alpha: 1,
64+
width: 200,
65+
height: 200,
66+
color: 0xff0000ff,
67+
pivot: 0,
68+
text: 'red',
69+
fontSize: 80,
70+
fontFamily: 'sans-serif',
71+
parent: boundaryRect,
72+
});
73+
74+
redText.on('outOfBounds', () => {
75+
console.log('red text out of bounds');
76+
redStatus.text = 'Red Status: text out of bounds';
77+
redStatus.color = 0xff0000ff;
78+
});
79+
80+
redText.on('inViewport', () => {
81+
console.log('red text in view port');
82+
redStatus.text = 'Red Status: text in view port';
83+
redStatus.color = 0x00ff00ff;
84+
});
85+
86+
redText.on('inBounds', () => {
87+
console.log('red text inside render bounds');
88+
redStatus.text = 'Red Status: text in bounds';
89+
redStatus.color = 0xffff00ff;
90+
});
91+
92+
const blueText = renderer.createTextNode({
93+
x: 1920 / 2 - 200,
94+
y: 100,
95+
alpha: 1,
96+
width: 200,
97+
height: 200,
98+
color: 0x0000ffff,
99+
pivot: 0,
100+
text: 'blue',
101+
fontSize: 80,
102+
fontFamily: 'sans-serif',
103+
parent: testRoot,
104+
});
105+
106+
blueText.on('outOfBounds', () => {
107+
console.log('blue text ouf ot bounds');
108+
blueStatus.text = 'Blue Status: blue text out of bounds';
109+
blueStatus.color = 0xff0000ff;
110+
});
111+
112+
blueText.on('inViewport', () => {
113+
console.log('blue text in view port');
114+
blueStatus.text = 'Blue Status: blue text in view port';
115+
blueStatus.color = 0x00ff00ff;
116+
});
117+
118+
blueText.on('inBounds', () => {
119+
console.log('blue text inside render bounds');
120+
blueStatus.text = 'Blue Status: blue text in bounds';
121+
blueStatus.color = 0xffff00ff;
122+
});
123+
124+
let runAnimation = false;
125+
const animate = async () => {
126+
redText
127+
.animate(
128+
{
129+
x: -500,
130+
},
131+
{
132+
duration: 4000,
133+
},
134+
)
135+
.start();
136+
137+
await blueText
138+
.animate(
139+
{
140+
x: -1200,
141+
},
142+
{
143+
duration: 4000,
144+
},
145+
)
146+
.start()
147+
.waitUntilStopped();
148+
149+
redText.x = 1920 + 400;
150+
blueText.x = 1920 + 400;
151+
152+
redText
153+
.animate(
154+
{
155+
x: 520,
156+
},
157+
{
158+
duration: 4000,
159+
},
160+
)
161+
.start();
162+
163+
await blueText
164+
.animate(
165+
{
166+
x: 1920 / 2 - 200,
167+
},
168+
{
169+
duration: 4000,
170+
},
171+
)
172+
.start()
173+
.waitUntilStopped();
174+
175+
if (runAnimation) {
176+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
177+
setTimeout(animate, 2000);
178+
}
179+
};
180+
181+
const moveModifier = 10;
182+
window.onkeydown = (e) => {
183+
if (e.key === ' ') {
184+
runAnimation = !runAnimation;
185+
186+
if (runAnimation) {
187+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
188+
animate();
189+
}
190+
}
191+
192+
if (e.key === 'ArrowRight') {
193+
redText.x += moveModifier;
194+
blueText.x += moveModifier;
195+
}
196+
197+
if (e.key === 'ArrowLeft') {
198+
redText.x -= moveModifier;
199+
blueText.x -= moveModifier;
200+
}
201+
202+
if (e.key === 'Enter') {
203+
runAnimation = false;
204+
redText.x = 520;
205+
blueText.x = 1920 / 2 - 200;
206+
}
207+
};
208+
}

src/core/CoreNode.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -647,7 +647,7 @@ export type CoreNodeAnimatableProps = {
647647
export class CoreNode extends EventEmitter {
648648
readonly children: CoreNode[] = [];
649649
protected _id: number = getNewId();
650-
protected props: Required<CoreNodeWritableProps>;
650+
readonly props: Required<CoreNodeWritableProps>;
651651

652652
public updateType = UpdateType.All;
653653

0 commit comments

Comments
 (0)