Skip to content

Instantly share code, notes, and snippets.

@unrealhoang
Created January 23, 2026 14:13
Show Gist options
  • Select an option

  • Save unrealhoang/84639f31989f66b10c093dfcb4fbc1aa to your computer and use it in GitHub Desktop.

Select an option

Save unrealhoang/84639f31989f66b10c093dfcb4fbc1aa to your computer and use it in GitHub Desktop.
compare different text rendering methods
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Text Rendering Benchmark - 100x100 ASCII</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: system-ui, sans-serif;
background: #1e1e1e;
color: #d4d4d4;
overflow: hidden;
}
.controls {
position: fixed;
top: 10px;
left: 10px;
right: 10px;
z-index: 1000;
padding: 15px;
background: rgba(37, 37, 38, 0.95);
border-radius: 4px;
display: flex;
gap: 15px;
align-items: center;
flex-wrap: wrap;
}
button {
padding: 8px 16px;
background: #0e639c;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
font-size: 14px;
}
button:hover {
background: #1177bb;
}
button:disabled {
background: #555;
cursor: not-allowed;
}
.stats {
display: flex;
gap: 20px;
font-family: 'Courier New', monospace;
font-size: 12px;
}
.stat-item {
display: flex;
gap: 5px;
}
.good { color: #4ec9b0; }
.warning { color: #ce9178; }
.bad { color: #f48771; }
.test-panel {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: none;
flex-direction: column;
background: #1e1e1e;
}
.test-panel.active {
display: flex;
}
.panel-header {
padding: 80px 20px 10px 20px;
background: #252526;
border-bottom: 1px solid #3e3e42;
}
.panel-header h2 {
color: #4ec9b0;
margin-bottom: 10px;
}
.panel-stats {
font-family: 'Courier New', monospace;
font-size: 13px;
display: flex;
gap: 20px;
}
.editor-container {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
overflow: hidden;
}
.editor {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'Courier New', monospace;
line-height: 1.0;
background: #1e1e1e;
color: #d4d4d4;
border: 1px solid #3e3e42;
padding: 0;
margin: 0;
white-space: pre;
overflow: hidden;
}
textarea.editor {
resize: none;
}
canvas {
border: 1px solid #3e3e42;
background: #1e1e1e;
}
</style>
</head>
<body>
<div class="controls">
<button id="startBtn">Start All Tests</button>
<button id="stopBtn" disabled>Stop Tests</button>
<button id="resetBtn">Reset</button>
<div class="stats">
<div class="stat-item">Target: <strong>60 FPS</strong></div>
<div class="stat-item">Duration: <strong>5s each</strong></div>
<div class="stat-item">Grid: <strong>100×100</strong></div>
</div>
</div>
<!-- Test Panel 1: <pre> -->
<div id="panel1" class="test-panel">
<div class="panel-header">
<h2>Method 1: &lt;pre&gt; Element (textContent)</h2>
<div class="panel-stats">
<div>FPS: <span id="fps1" class="good">--</span></div>
<div>Frame Time: <span id="time1">--</span></div>
<div>Dropped: <span id="dropped1">--</span></div>
<div>Font: <span id="font1">--</span></div>
</div>
</div>
<div class="editor-container">
<pre id="editor1" class="editor"></pre>
</div>
</div>
<!-- Test Panel 2: <textarea> -->
<div id="panel2" class="test-panel">
<div class="panel-header">
<h2>Method 2: &lt;textarea&gt;</h2>
<div class="panel-stats">
<div>FPS: <span id="fps2" class="good">--</span></div>
<div>Frame Time: <span id="time2">--</span></div>
<div>Dropped: <span id="dropped2">--</span></div>
<div>Font: <span id="font2">--</span></div>
</div>
</div>
<div class="editor-container">
<textarea id="editor2" class="editor"></textarea>
</div>
</div>
<!-- Test Panel 3: Canvas -->
<div id="panel3" class="test-panel">
<div class="panel-header">
<h2>Method 3: Canvas (fillText)</h2>
<div class="panel-stats">
<div>FPS: <span id="fps3" class="good">--</span></div>
<div>Frame Time: <span id="time3">--</span></div>
<div>Dropped: <span id="dropped3">--</span></div>
<div>Font: <span id="font3">--</span></div>
</div>
</div>
<div class="editor-container">
<canvas id="editor3"></canvas>
</div>
</div>
<script>
const ROWS = 100;
const COLS = 400;
const TOTAL_CHARS = ROWS * COLS;
const TEST_DURATION = 5000; // 5 seconds
const TARGET_FPS = 120;
const TARGET_FRAME_TIME = 1000 / TARGET_FPS;
// ASCII characters to randomly pick from
const ASCII_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-=[]{}|;:,.<>?/~ ';
// Calculate optimal font size to fit 100x100 grid
function calculateFontSize() {
const headerHeight = 120; // Approximate header height
const padding = 40;
const availableHeight = window.innerHeight - headerHeight - padding;
const availableWidth = window.innerWidth - padding;
// Monospace characters are roughly 0.6 width/height ratio
const charAspectRatio = 0.6;
// Calculate font size based on height constraint
const fontByHeight = Math.floor(availableHeight / ROWS);
// Calculate font size based on width constraint
const fontByWidth = Math.floor(availableWidth / (COLS * charAspectRatio));
// Use the smaller of the two to ensure everything fits
return Math.min(fontByHeight, fontByWidth);
}
// Generate random text
function generateRandomText() {
let text = '';
for (let row = 0; row < ROWS; row++) {
for (let col = 0; col < COLS; col++) {
text += ASCII_CHARS[Math.floor(Math.random() * ASCII_CHARS.length)];
}
if (row < ROWS - 1) text += '\n';
}
return text;
}
// Performance tracker
class PerfTracker {
constructor(id) {
this.id = id;
this.frameTimes = [];
this.lastTime = 0;
this.frameCount = 0;
this.droppedFrames = 0;
}
frame(timestamp) {
if (this.lastTime) {
const frameTime = timestamp - this.lastTime;
this.frameTimes.push(frameTime);
if (frameTime > TARGET_FRAME_TIME * 1.5) {
this.droppedFrames++;
}
}
this.lastTime = timestamp;
this.frameCount++;
}
getStats() {
if (this.frameTimes.length === 0) return null;
const avgFrameTime = this.frameTimes.reduce((a, b) => a + b, 0) / this.frameTimes.length;
const fps = 1000 / avgFrameTime;
return {
fps: fps.toFixed(2),
avgFrameTime: avgFrameTime.toFixed(2),
droppedFrames: this.droppedFrames,
totalFrames: this.frameCount
};
}
updateUI() {
const stats = this.getStats();
if (!stats) return;
const fpsEl = document.getElementById(`fps${this.id}`);
const timeEl = document.getElementById(`time${this.id}`);
const droppedEl = document.getElementById(`dropped${this.id}`);
fpsEl.textContent = stats.fps;
fpsEl.className = stats.fps >= 58 ? 'good' : stats.fps >= 45 ? 'warning' : 'bad';
timeEl.textContent = `${stats.avgFrameTime}ms`;
droppedEl.textContent = `${stats.droppedFrames} / ${stats.totalFrames}`;
}
}
// Calculate and set font sizes
const fontSize = calculateFontSize();
console.log(`Calculated font size: ${fontSize}px`);
// Method 1: <pre> element
const editor1 = document.getElementById('editor1');
editor1.style.fontSize = `${fontSize}px`;
document.getElementById('font1').textContent = `${fontSize}px`;
function updatePre(text) {
editor1.textContent = text;
}
// Method 2: <textarea>
const editor2 = document.getElementById('editor2');
editor2.style.fontSize = `${fontSize}px`;
editor2.style.width = `${COLS * fontSize * 0.6 + 10}px`;
editor2.style.height = `${ROWS * fontSize + 10}px`;
document.getElementById('font2').textContent = `${fontSize}px`;
function updateTextarea(text) {
editor2.value = text;
}
// Method 3: Canvas
const canvas = document.getElementById('editor3');
const ctx = canvas.getContext('2d');
// Set canvas size based on font size
const charWidth = fontSize * 0.6;
const charHeight = fontSize;
canvas.width = Math.ceil(COLS * charWidth) + 2;
canvas.height = Math.ceil(ROWS * charHeight) + 2;
ctx.font = `${fontSize}px "Monaco", "Menlo", "Ubuntu Mono", "Consolas", "Courier New", monospace`;
ctx.textBaseline = 'top';
document.getElementById('font3').textContent = `${fontSize}px`;
function updateCanvas(text) {
ctx.fillStyle = '#1e1e1e';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#d4d4d4';
const lines = text.split('\n');
for (let row = 0; row < lines.length; row++) {
const line = lines[row];
for (let col = 0; col < line.length; col++) {
ctx.fillText(line[col], col * charWidth + 1, row * charHeight + 1);
}
}
}
// Panel management
function showPanel(num) {
document.querySelectorAll('.test-panel').forEach(p => p.classList.remove('active'));
document.getElementById(`panel${num}`).classList.add('active');
}
// Test runner
let running = false;
async function runTest(methodNum, updateFn, duration = TEST_DURATION) {
showPanel(methodNum);
const tracker = new PerfTracker(methodNum);
const startTime = performance.now();
return new Promise((resolve) => {
function frame(timestamp) {
if (!running) {
resolve(tracker);
return;
}
const elapsed = timestamp - startTime;
if (elapsed >= duration) {
resolve(tracker);
return;
}
// Generate and set new text
const text = generateRandomText();
updateFn(text);
tracker.frame(timestamp);
tracker.updateUI();
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
});
}
// Control buttons
const startBtn = document.getElementById('startBtn');
const stopBtn = document.getElementById('stopBtn');
const resetBtn = document.getElementById('resetBtn');
startBtn.addEventListener('click', async () => {
running = true;
startBtn.disabled = true;
stopBtn.disabled = false;
resetBtn.disabled = true;
// Run tests sequentially
console.log('Testing Method 1: <pre>');
await runTest(1, updatePre);
if (!running) {
showPanel(1);
return;
}
console.log('Testing Method 2: <textarea>');
await runTest(2, updateTextarea);
if (!running) {
showPanel(1);
return;
}
console.log('Testing Method 3: Canvas');
await runTest(3, updateCanvas);
running = false;
startBtn.disabled = false;
stopBtn.disabled = true;
resetBtn.disabled = false;
showPanel(1);
console.log('All tests complete!');
});
stopBtn.addEventListener('click', () => {
running = false;
startBtn.disabled = false;
stopBtn.disabled = true;
resetBtn.disabled = false;
});
resetBtn.addEventListener('click', () => {
['1', '2', '3'].forEach(id => {
document.getElementById(`fps${id}`).textContent = '--';
document.getElementById(`time${id}`).textContent = '--';
document.getElementById(`dropped${id}`).textContent = '--';
});
editor1.textContent = '';
editor2.value = '';
ctx.fillStyle = '#1e1e1e';
ctx.fillRect(0, 0, canvas.width, canvas.height);
showPanel(1);
});
// Initialize with some content
showPanel(1);
const initialText = generateRandomText();
updatePre(initialText);
updateTextarea(initialText);
updateCanvas(initialText);
// Handle window resize
let resizeTimeout;
window.addEventListener('resize', () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
location.reload();
}, 500);
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment