Created
January 23, 2026 14:13
-
-
Save unrealhoang/84639f31989f66b10c093dfcb4fbc1aa to your computer and use it in GitHub Desktop.
compare different text rendering methods
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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: <pre> 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: <textarea></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