Skip to content

Commit 665e44b

Browse files
committed
Tentative Canvas “Text Rendering” test
1 parent 806c1df commit 665e44b

File tree

3 files changed

+335
-0
lines changed

3 files changed

+335
-0
lines changed

MotionMark/resources/debug-runner/tests.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,10 @@ Suites.push(new Suite("Tentative 1.4 suite",
475475
{
476476
url: "dev/dashboard/dashboard.html",
477477
name: "Dashboard"
478+
},
479+
{
480+
url: "dev/text-rendering.html",
481+
name: "Text Rendering"
478482
}
479483
]
480484
));
Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
/*
2+
* Copyright (C) 2025 Apple Inc. All rights reserved.
3+
*
4+
* Redistribution and use in source and binary forms, with or without
5+
* modification, are permitted provided that the following conditions
6+
* are met:
7+
* 1. Redistributions of source code must retain the above copyright
8+
* notice, this list of conditions and the following disclaimer.
9+
* 2. Redistributions in binary form must reproduce the above copyright
10+
* notice, this list of conditions and the following disclaimer in the
11+
* documentation and/or other materials provided with the distribution.
12+
*
13+
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15+
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16+
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17+
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18+
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22+
* ARISING IN ANY WAY OUT of THE USE of THIS SOFTWARE, EVEN IF ADVISED of
23+
* THE POSSIBILITY of SUCH DAMAGE.
24+
*/
25+
26+
// === LOREM IPSUM GENERATOR ===
27+
28+
const LoremIpsum = {
29+
_words: [
30+
'lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing', 'elit', 'curabitur', 'vel', 'hendrerit', 'libero',
31+
'eleifend', 'blandit', 'nunc', 'ornare', 'odio', 'ut', 'orci', 'gravida', 'imperdiet', 'nullam', 'purus', 'lacinia', 'a',
32+
'pretium', 'quis', 'congue', 'praesent', 'sagittis', 'laoreet', 'auctor', 'mauris', 'non', 'velit', 'eros', 'dictum',
33+
'proin', 'accumsan', 'sapien', 'nec', 'massa', 'volutpat', 'venenatis', 'sed', 'eu', 'molestie', 'lacus', 'quisque',
34+
'porttitor', 'ligula', 'dui', 'mollis', 'tempus', 'at', 'magna', 'vestibulum', 'turpis', 'ac', 'diam', 'tincidunt',
35+
'id', 'condimentum', 'enim', 'sodales', 'in', 'hac', 'habitasse', 'platea', 'dictumst', 'aenean', 'neque', 'fusce',
36+
'augue', 'leo', 'eget', 'semper', 'mattis', 'tortor', 'scelerisque', 'nulla', 'interdum', 'tellus', 'malesuada',
37+
'rhoncus', 'porta', 'sem', 'aliquet', 'et', 'nam', 'suspendisse', 'potenti', 'vivamus', 'luctus', 'fringilla', 'erat',
38+
],
39+
40+
generate(wordCount) {
41+
let words = [];
42+
let sentenceIndex = 0;
43+
while (words.length < wordCount) {
44+
const sentenceLength = Math.floor(Math.random() * 12) + 8; // 8 to 20 words
45+
for (let i = 0; i < sentenceLength && words.length < wordCount; i++) {
46+
let word = this._words[Math.floor(Math.random() * this._words.length)];
47+
if (i === 0) {
48+
word = word.charAt(0).toUpperCase() + word.slice(1);
49+
}
50+
if (i === sentenceLength - 1) {
51+
word += '.';
52+
}
53+
const endOfParagraph = (i === sentenceLength - 1) && (Math.random() < 0.1);
54+
words.push({ word, endOfParagraph, sentenceIndex });
55+
}
56+
sentenceIndex++;
57+
}
58+
return words;
59+
}
60+
};
61+
62+
// === TEXT LAYOUT ===
63+
64+
class TextLayout {
65+
constructor(words, context, pageWidth, pageHeight, fontSize) {
66+
this.words = words;
67+
this.context = context;
68+
this.pageWidth = pageWidth;
69+
this.pageHeight = pageHeight;
70+
this.pageMargin = 20;
71+
this.lineHeight = 1.2;
72+
this.fontSize = fontSize;
73+
this.pages = this._layoutPages();
74+
}
75+
76+
_layoutPages() {
77+
const pages = [];
78+
this.context.font = `${this.fontSize}px sans-serif`;
79+
80+
const drawableWidth = this.pageWidth - this.pageMargin * 2;
81+
const drawableHeight = this.pageHeight - this.pageMargin * 2;
82+
83+
if (this.words.length === 0)
84+
return pages;
85+
86+
let currentPageWords = [];
87+
let x = this.pageMargin;
88+
let y = this.pageMargin + this.fontSize;
89+
90+
for (const wordData of this.words) {
91+
const word = wordData.word;
92+
const wordWidth = this.context.measureText(word + ' ').width;
93+
94+
if (x + wordWidth > drawableWidth + this.pageMargin) {
95+
x = this.pageMargin;
96+
y += this.fontSize * this.lineHeight;
97+
}
98+
99+
if (y > drawableHeight) {
100+
pages.push(currentPageWords);
101+
currentPageWords = [];
102+
x = this.pageMargin;
103+
y = this.pageMargin + this.fontSize;
104+
}
105+
106+
currentPageWords.push({ text: word, x, y, width: wordWidth, sentenceIndex: wordData.sentenceIndex });
107+
x += wordWidth;
108+
109+
if (wordData.endOfParagraph) {
110+
x = this.pageMargin;
111+
y += this.fontSize * this.lineHeight * 2;
112+
}
113+
}
114+
115+
if (currentPageWords.length > 0) {
116+
pages.push(currentPageWords);
117+
}
118+
119+
return pages;
120+
}
121+
}
122+
123+
124+
// === STAGE ===
125+
126+
class TextRenderingStage extends Stage {
127+
async initialize(benchmark, options) {
128+
await super.initialize(benchmark, options);
129+
130+
this.context = this.element.getContext('2d');
131+
this.words = LoremIpsum.generate(100000);
132+
this._complexity = 0;
133+
this.numPagesToRender = 0;
134+
135+
// Virtual dimensions
136+
this.virtualDPI = 96;
137+
this.virtualPageWidth = 8.5 * this.virtualDPI;
138+
this.virtualPageHeight = 11 * this.virtualDPI;
139+
this.virtualFontSize = (8 / 72) * this.virtualDPI; // 8pt font
140+
141+
// Perform a single, full layout on the virtual pages.
142+
this.virtualLayout = new TextLayout(this.words, this.context, this.virtualPageWidth, this.virtualPageHeight, this.virtualFontSize);
143+
144+
// Assign highlight colors and styles to each word
145+
const highlightColors = ['#FFFFFF', '#FFFFFF', '#FFFFFF', '#FFFF99', '#99FF99', '#99FFFF', '#FF99FF'];
146+
const styles = ['bold', 'italic', 'underline'];
147+
this.sentenceColors = [];
148+
149+
this.virtualLayout.pages.flat().forEach(word => {
150+
// Assign sentence color
151+
if (!this.sentenceColors[word.sentenceIndex]) {
152+
this.sentenceColors[word.sentenceIndex] = highlightColors[Math.floor(Math.random() * highlightColors.length)];
153+
}
154+
155+
// Assign word style
156+
if (Math.random() < 0.75) {
157+
word.style = 'normal';
158+
} else {
159+
word.style = styles[Math.floor(Math.random() * styles.length)];
160+
}
161+
});
162+
}
163+
164+
tune(count) {
165+
this._complexity = Math.max(0, this._complexity + count);
166+
167+
let wordsCounted = 0;
168+
let pages = 0;
169+
for (const page of this.virtualLayout.pages) {
170+
wordsCounted += page.length;
171+
pages++;
172+
if (wordsCounted >= this._complexity)
173+
break;
174+
}
175+
this.numPagesToRender = pages;
176+
}
177+
178+
animate() {
179+
const context = this.context;
180+
const stageSize = this.size;
181+
182+
// Determine grid and page dimensions
183+
let bestGrid = { cols: 0, rows: 0, aspectRatioDiff: Infinity };
184+
const stageAspectRatio = stageSize.x / stageSize.y;
185+
const pageAspectRatio = this.virtualPageWidth / this.virtualPageHeight;
186+
const gapToPageHeightRatio = 0.1;
187+
const numPages = this.numPagesToRender;
188+
189+
if (numPages === 0) {
190+
context.clearRect(0, 0, stageSize.x, stageSize.y);
191+
return;
192+
}
193+
194+
for (let cols = 1; cols <= numPages; cols++) {
195+
const rows = Math.ceil(numPages / cols);
196+
const gridAspectRatio = (cols * pageAspectRatio + (cols + 1) * gapToPageHeightRatio * pageAspectRatio) / (rows + (rows + 1) * gapToPageHeightRatio);
197+
const aspectRatioDiff = Math.abs(gridAspectRatio - stageAspectRatio);
198+
if (aspectRatioDiff < bestGrid.aspectRatioDiff) {
199+
bestGrid = { cols, rows, aspectRatioDiff };
200+
}
201+
}
202+
203+
const { cols, rows } = bestGrid;
204+
205+
let actualPageHeight, actualPageWidth, gap;
206+
const gridAspectRatio = (cols * pageAspectRatio + (cols + 1) * gapToPageHeightRatio * pageAspectRatio) / (rows + (rows + 1) * gapToPageHeightRatio);
207+
if (stageAspectRatio > gridAspectRatio) { // Height is constrained
208+
actualPageHeight = stageSize.y / (rows + (rows + 1) * gapToPageHeightRatio);
209+
} else { // Width is constrained
210+
actualPageHeight = stageSize.x / (cols * pageAspectRatio + (cols + 1) * gapToPageHeightRatio * pageAspectRatio);
211+
}
212+
actualPageWidth = actualPageHeight * pageAspectRatio;
213+
gap = actualPageHeight * gapToPageHeightRatio;
214+
215+
const scale = actualPageHeight / this.virtualPageHeight;
216+
const scaledFontSize = this.virtualFontSize * scale;
217+
const scaledLineHeight = this.virtualLayout.lineHeight * scaledFontSize;
218+
219+
// Draw background
220+
context.fillStyle = 'lightgray';
221+
context.fillRect(0, 0, stageSize.x, stageSize.y);
222+
223+
const totalGridWidth = cols * actualPageWidth + (cols - 1) * gap;
224+
const totalGridHeight = rows * actualPageHeight + (rows - 1) * gap;
225+
const startX = (stageSize.x - totalGridWidth) / 2;
226+
const startY = (stageSize.y - totalGridHeight) / 2;
227+
228+
let wordsDrawn = 0;
229+
for (let i = 0; i < numPages; i++) {
230+
const pageData = this.virtualLayout.pages[i];
231+
const pageColumn = i % cols;
232+
const pageRow = Math.floor(i / cols);
233+
const pageX = startX + pageColumn * (actualPageWidth + gap);
234+
const pageY = startY + pageRow * (actualPageHeight + gap);
235+
236+
// Draw page
237+
context.fillStyle = 'white';
238+
context.fillRect(pageX, pageY, actualPageWidth, actualPageHeight);
239+
context.strokeStyle = 'black';
240+
context.lineWidth = 1;
241+
context.strokeRect(pageX, pageY, actualPageWidth, actualPageHeight);
242+
243+
// Draw text and highlights
244+
for (const word of pageData) {
245+
if (wordsDrawn >= this._complexity) break;
246+
247+
const scaledX = pageX + word.x * scale;
248+
const scaledY = pageY + word.y * scale;
249+
const scaledWidth = word.width * scale;
250+
251+
// Highlight
252+
context.fillStyle = this.sentenceColors[word.sentenceIndex];
253+
context.fillRect(scaledX, scaledY - scaledFontSize, scaledWidth, scaledLineHeight);
254+
255+
// Text
256+
let fontStyle = '';
257+
if (word.style === 'bold') fontStyle = 'bold ';
258+
if (word.style === 'italic') fontStyle = 'italic ';
259+
context.font = `${fontStyle}${scaledFontSize}px sans-serif`;
260+
context.fillStyle = 'black';
261+
context.fillText(word.text, scaledX, scaledY);
262+
263+
// Underline
264+
if (word.style === 'underline') {
265+
const underlineHeight = 1 * scale;
266+
context.fillRect(scaledX, scaledY + 2 * scale, scaledWidth, underlineHeight);
267+
}
268+
269+
wordsDrawn++;
270+
}
271+
if (wordsDrawn >= this._complexity) break;
272+
}
273+
}
274+
275+
complexity() {
276+
return this._complexity;
277+
}
278+
}
279+
280+
// === BENCHMARK ===
281+
282+
class TextRenderingBenchmark extends Benchmark {
283+
constructor(options) {
284+
super(new TextRenderingStage(), options);
285+
}
286+
}
287+
288+
window.benchmarkClass = TextRenderingBenchmark;
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<!--
2+
Copyright (C) 2025 Apple Inc. All rights reserved.
3+
4+
Redistribution and use in source and binary forms, with or without
5+
modification, are permitted provided that the following conditions
6+
are met:
7+
1. Redistributions of source code must retain the above copyright
8+
notice, this list of conditions and the following disclaimer.
9+
2. Redistributions in binary form must reproduce the above copyright
10+
notice, this list of conditions and the following disclaimer in the
11+
documentation and/or other materials provided with the distribution.
12+
13+
THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15+
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16+
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17+
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18+
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19+
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20+
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21+
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22+
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23+
THE POSSIBILITY OF SUCH DAMAGE.
24+
-->
25+
<!DOCTYPE html>
26+
<html>
27+
<head>
28+
<meta charset="utf-8">
29+
<link rel="stylesheet" type="text/css" href="../resources/stage.css">
30+
</head>
31+
<body>
32+
<canvas id="stage"></canvas>
33+
<script src="../../resources/strings.js"></script>
34+
<script src="../../resources/extensions.js"></script>
35+
<script src="../../resources/statistics.js"></script>
36+
<script src="../resources/math.js"></script>
37+
<script src="../resources/benchmark.js"></script>
38+
<script src="../resources/controllers.js"></script>
39+
<script src="../resources/stage.js"></script>
40+
<script src="../core/resources/canvas-stage.js"></script>
41+
<script src="resources/text-rendering.js"></script>
42+
</body>
43+
</html>

0 commit comments

Comments
 (0)