This document provides a comprehensive overview of the teXt0wnz architecture, including application structure, data flow, module organization, and key design decisions.
- High-Level Overview
- Application Modes
- Client Architecture
- Server Architecture
- Data Flow
- Module Structure
- Build System
- Storage and Persistence
- Design Patterns
- Performance Optimizations
teXt0wnz is a Progressive Web Application (PWA) for creating and editing text-mode artwork (ANSI, ASCII, XBIN, NFO). The application operates in two distinct modes:
- Client-only mode - Standalone editor with local storage
- Collaborative mode - Real-time multi-user editing via WebSocket server
┌────────────────────────────────────────────────────────┐
│ Browser Client │
│ ┌─────────────┐ ┌──────────────┐ ┌───────────────┐ │
│ │ UI Layer │ │ Canvas Layer │ │ Storage Layer │ │
│ │ (Controls) │ │ (Rendering) │ │ (IndexedDB) │ │
│ └─────────────┘ └──────────────┘ └───────────────┘ │
│ │ │ │ │
│ └────────────────┴───────────────────┘ │
│ │ │
│ State Management │
│ │ │
└──────────────────────────┼─────────────────────────────┘
│
┌────────┼────────┐
│ │ │
Service Worker │ File System APIs
(Offline/Share) │ (File Handlers)
│
│
Optional WebSocket
│
┌──────────────────────────┼────────────────────────────┐
│ Collaboration Server │
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ WebSocket │ │ Session Mgmt │ │ File Storage │ │
│ │ Handlers │ │ (Canvas) │ │ (Disk) │ │
│ └─────────────┘ └──────────────┘ └──────────────┘ │
└───────────────────────────────────────────────────────┘
The default mode when no server is detected or when user chooses local mode.
Features:
- Full drawing and editing capabilities
- Local storage persistence (IndexedDB)
- Automatic save/restore
- File import/export
- Offline PWA support
Data Flow:
User Action → State Update → Canvas Render → IndexedDB Persist
Activated when connecting to a collaboration server.
Features:
- All client-only features plus:
- Real-time multi-user editing
- Synchronized canvas state
- Collaborative chat
- Server-side persistence
- Session management
Data Flow:
User Action → State Update → Canvas Render → WebSocket Send → Server Broadcast → Other Clients
┌──────────────────────────────────────────────────────┐
│ Presentation Layer │
│ UI Components, Modals, Toolbars, Palettes │
└─────────────────┬────────────────────────────────────┘
│
┌─────────────────┴────────────────────────────────────┐
│ Application Layer │
│ Event Handlers, Tool Controllers, State Management │
└─────────────────┬────────────────────────────────────┘
│
┌─────────────────┴────────────────────────────────────┐
│ Canvas Layer │
│ Rendering Engine, Font Management, Dirty Tracking │
└─────────────────┬────────────────────────────────────┘
│
┌─────────────────┴────────────────────────────────────────────────────┐
│ Data Layer │
│ Storage (IndexedDB), File I/O, Network (WebSocket), PWA (Caching) │
└──────────────────────────────────────────────────────────────────────┘
State Management (state.js)
- Global application state
- Canvas dimensions and configuration
- Current tool and color selection
- Font and palette management
- Undo/redo history
Canvas Rendering (canvas.js)
- Offscreen canvas for performance
- Dirty region tracking (only redraw changed areas)
- Character and color rendering
- Mirror mode support
- Grid overlay
Font System (font.js, lazyFont.js, fontCache.js)
- PNG-based bitmap fonts
- Lazy loading on demand
- Font caching for performance
- Support for 100+ classic fonts
- Letter spacing (9px mode)
Drawing Tools (freehandTools.js)
- Halfblock/Block drawing
- Character brush
- Shading brush (░▒▓)
- Line tool with conflict resolution
- Shape tools (rectangle, circle/ellipse)
- Fill tool with smart attributes
- Selection tool with transformations
- Sample tool (color picker)
Keyboard Mode (keyboard.js)
- Text input handling
- Arrow key navigation
- Special character insertion (F-keys)
- Canvas editing shortcuts
User Interface (ui.js)
- Modal dialogs
- Toolbar management
- Menu systems
- Color palette UI
- Character picker
- Status bar updates
File Operations (file.js)
- ANSI format (.ans, .utf8.ans)
- Binary format (.bin)
- XBIN format (.xb)
- Scene release formats (.nfo, .diz)
- Plain text (.txt)
- Full SAUCE metadata support
- PNG export
File Opening Methods:
- Traditional file picker (all platforms)
- Drag-and-drop (all platforms)
- OS "Open with" (Desktop Chrome/Edge via File Handlers API)
- Share sheet (Android via Share Target API)
- iOS workaround (
accept="*/*"for broader file access)
Service Worker (service.js)
- Offline support and caching
- Share Target API (Android file sharing)
- Runtime caching strategies
- Workbox-based precaching
- Stale file cleanup
PWA Capabilities (site.webmanifest)
- File Handlers API (Desktop "Open with" support)
- Share Target API (Mobile share sheet integration)
- Multi-platform file opening:
- Desktop: OS "Open with" → File Handlers API
- Android: Share sheet → Share Target API
- iOS: Manual file picker with
accept="*/*"hack
Color Management (palette.js)
- 16-color ANSI palette
- ICE colors (extended backgrounds)
- RGB to ANSI conversion
- Color conflict resolution
- Custom palettes (XBIN)
Storage (storage.js, compression.js)
- IndexedDB for canvas persistence
- Optimized binary compression
- Automatic save/restore
- Editor settings persistence
- Run-length encoding for efficiency
Network (network.js, websocket.js)
- WebSocket client (in Web Worker with security hardening)
- Mandatory worker initialization sequence
- Trusted URL construction from page location
- Silent connection checks (non-intrusive)
- Connection state management
- Message protocol handling with input validation
- Canvas synchronization
- Chat functionality
Custom events for canvas interaction:
document.addEventListener('onTextCanvasDown', handler);
document.addEventListener('onTextCanvasDrag', handler);
document.addEventListener('onTextCanvasUp', handler);This abstraction allows tools to work consistently across:
- Mouse events
- Touch events
- Keyboard events (for cursor position)
Each drawing tool follows this pattern:
const createToolController = () => {
function enable() {
// Register event listeners
document.addEventListener('onTextCanvasDown', canvasDown);
document.addEventListener('onTextCanvasDrag', canvasDrag);
document.addEventListener('onTextCanvasUp', canvasUp);
}
function disable() {
// Unregister event listeners
document.removeEventListener('onTextCanvasDown', canvasDown);
document.removeEventListener('onTextCanvasDrag', canvasDrag);
document.removeEventListener('onTextCanvasUp', canvasUp);
}
return {
enable: enable,
disable: disable,
};
};Benefits:
- Clean enable/disable without conflicts
- Consistent interface for all tools
- Easy tool switching
- Memory leak prevention
┌─────────────────────────────────────────────────────────┐
│ HTTP/HTTPS Server │
│ (Express 5.x) │
└─────────────────┬───────────────────────────────────────┘
│
┌─────────────────┴───────────────────────────────────────┐
│ Session Middleware │
│ (express-session) │
└─────────────────┬───────────────────────────────────────┘
│
┌─────────────────┴───────────────────────────────────────┐
│ WebSocket Routes │
│ / (direct connections) │
│ /server (proxied connections) │
└─────────────────┬───────────────────────────────────────┘
│
┌─────────────────┴───────────────────────────────────────┐
│ Collaboration Engine │
│ (text0wnz.js) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Canvas State │ │ User Sessions│ │ Broadcasting │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────┘
Configuration (config.js)
- Parse CLI arguments
- Validate options
- Provide defaults
- Export configuration object
Server Setup (server.js)
- Express server initialization
- SSL/TLS configuration
- Session middleware setup
- WebSocket routing
- Error handling
Collaboration Engine (text0wnz.js)
- Canvas state management (imageData object)
- User session tracking
- Message broadcasting
- State persistence
- Canvas settings synchronization
WebSocket Handling (websockets.js)
- Connection/disconnection handlers
- Message routing
- User cleanup
- Error handling
- Logging
File I/O (fileio.js)
- Binary file operations
- SAUCE record creation/parsing
- Canvas dimension extraction
- Format conversions
- Timestamped backups
Utilities (utils.js)
- Logging helpers
- Data validation
- Type conversions
- Helper functions
Client to Server:
const clientProto =
(['join', username], // Join session
['nick', newUsername], // Change username
['chat', message], // Send chat message
['draw', blocks], // Drawing command
['resize', { columns, rows }], // Canvas resize
['fontChange', { fontName }], // Font change
['iceColorsChange', { iceColors }], // ICE colors toggle
['letterSpacingChange', { letterSpacing }]); // Letter spacing toggleServer to Client:
start is the first command run after websocket initialization. It returns the connecting client's session id and the entire shared server state, which the editor caches. If the client chooses to join, the editor is reconfigured with the shared server data and chat features are enabled.
const serverProto =
(['start', sessionData, sessionID, userList], // Canvas data, client id, users
['join', username, sessionID], // User joined
['part', sessionID], // User left
['nick', username, sessionID], // Username changed
['chat', username, message], // Chat message
['draw', blocks], // Drawing broadcast
['resize', { columns, rows }]); // Canvas resizeChat Message Display:
The client displays chat messages differently based on context:
- User messages: Displayed with username handle and message text
- Server log messages: Join/leave/nick change events styled as system logs
When a user joins:
- Server sends current canvas state via "start" message
- Client applies canvas settings (size, font, colors, spacing)
- Client renders canvas from imageData
- User is added to session list
When a drawing occurs:
- Client sends "draw" message with affected blocks
- Server updates internal imageData
- Server broadcasts to all other clients
- Other clients update their canvas
┌─────────────┐
│ User Action │ (mouse, touch, keyboard)
└──────┬──────┘
│
▼
┌─────────────┐
│ Tool Handler│ (freehandTools.js)
└──────┬──────┘
│
▼
┌─────────────┐
│ Calculate │ (coords, colors, chars)
│ Changes │
└──────┬──────┘
│
▼
┌─────────────┐
│ Update │ (State.textArtCanvas)
│ Canvas │
└──────┬──────┘
│
├──────────────────┐
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Render │ │ Network │ (if collaborative)
│ (canvas) │ │ (websocket)│
└─────────────┘ └──────┬──────┘
│
▼
┌─────────────┐
│ Server │
│ Broadcast │
└──────┬──────┘
│
▼
┌─────────────┐
│Other Clients│
│ Render │
└─────────────┘
┌─────────────┐
│ User Clicks │ (Save button)
│ Save │
└──────┬──────┘
│
▼
┌─────────────┐
│ Get Canvas │ (State.textArtCanvas)
│ Data │
└──────┬──────┘
│
▼
┌─────────────┐
│ Format │ (ANSI, BIN, XBIN, etc.)
│ Converter │
└──────┬──────┘
│
▼
┌─────────────┐
│ Add │ (if enabled)
│ SAUCE │
└──────┬──────┘
│
▼
┌─────────────┐
│ Download │ (Browser download)
│ File │
└─────────────┘
┌─────────────┐
│ Canvas │ (change detected)
│ Changes │
└──────┬──────┘
│
▼
┌─────────────┐
│ Compress │ (RLE compression)
│ Data │
└──────┬──────┘
│
▼
┌─────────────┐
│ Save to │ (with debouncing)
│ IndexedDB │
└─────────────┘
src/
├── js/client/
│ ├── main.js # Application entry point
│ ├── state.js # Global state management
│ ├── canvas.js # Canvas rendering engine
│ ├── ui.js # User interface components
│ ├── toolbar.js # Toolbar management
│ ├── palette.js # Color palette
│ ├── keyboard.js # Keyboard mode and shortcuts
│ ├── freehandTools.js # Drawing tools
│ ├── file.js # File I/O operations
│ ├── network.js # Network communication
│ ├── websocket.js # WebSocket worker
│ ├── font.js # Font loading and rendering
│ ├── lazyFont.js # Lazy font loading
│ ├── fontCache.js # Font caching
│ ├── storage.js # IndexedDB persistence
│ ├── compression.js # Data compression
│ └── magicNumbers.js # Constants and magic values
└── service.js # PWA service worker
src/js/server/
├── main.js # Entry point
├── config.js # Configuration
├── server.js # Express server
├── text0wnz.js # Collaboration engine
├── websockets.js # WebSocket handlers
├── fileio.js # File operations
└── utils.js # Utilities
main.js
├── state.js
├── canvas.js
│ ├── font.js
│ ├── lazyFont.js
│ └── fontCache.js
├── ui.js
│ └── state.js
├── toolbar.js
├── palette.js
├── keyboard.js
│ ├── state.js
│ └── canvas.js
├── freehandTools.js
│ ├── state.js
│ ├── canvas.js
│ └── toolbar.js
├── file.js
│ ├── state.js
│ └── canvas.js
├── network.js
│ ├── state.js
│ ├── canvas.js
│ └── websocket.js (web worker)
├── storage.js
│ ├── state.js
│ └── compression.js
└── magicNumbers.js
Code Splitting:
manualChunks: {
core: ['state', 'storage', 'compression', 'ui'],
canvas: ['canvas', 'font', 'lazyFont', 'fontCache'],
tools: ['freehandTools', 'keyboard', 'toolbar'],
fileops: ['file'],
network: ['network'],
palette: ['palette']
}Benefits:
- Faster initial load (progressive loading)
- Better caching (chunks change independently)
- Smaller bundle sizes
- Parallel downloads
Build Output:
dist/
├── index.html
├── ui/js/
│ ├── editor-[hash].js # Entry point (~20 KB)
│ ├── core-[hash].js # Core modules (~80 KB)
│ ├── canvas-[hash].js # Canvas system (~60 KB)
│ ├── tools-[hash].js # Drawing tools (~100 KB)
│ ├── fileops-[hash].js # File operations (~40 KB)
│ ├── network-[hash].js # Collaboration (~30 KB)
│ ├── palette-[hash].js # Palette (~15 KB)
│ └── websocket.js # Worker (no hash)
└── ui/
├── stylez-[hash].css # Styles (~30 KB compressed)
├── fonts/ # Bitmap fonts (~2 MB)
└── img/ # Images (~500 KB)
CSS:
- Tailwind JIT compilation
- PostCSS processing
- cssnano minification
- Unused style purging
JavaScript:
- Terser minification
- Tree shaking
- Code splitting
- Source maps (dev only)
Images:
- PNG optimization (fonts)
- SVG sprite generation
Database: textArtDB
Object Stores:
-
canvasData- Key:
currentCanvas - Value: Compressed canvas data
- Updates: Debounced (500ms after last change)
- Key:
-
editorSettings- Key: Various setting names
- Values: User preferences
- Examples:
selectedFont,iceColors,letterSpacing,gridVisible
Compression:
- Run-length encoding (RLE)
- Stores only changed regions
- Typical compression: 90%+ for most artwork
Auto-Save Strategy:
// Debounced save after changes
const saveToIndexedDB = debounce(() => {
const compressed = compress(canvasData);
db.put('canvasData', compressed, 'currentCanvas');
}, 500);Purpose: Temporary storage for shared files and offline support
Cache: text0wnz-shared-files
Usage:
- Stores files shared via Share Target API (Android)
- Temporary cache cleared after file is opened
- Stale file cleanup on service worker activation
Cache Strategy:
// Service worker handles POST to /open
self.addEventListener('fetch', event => {
if (url.pathname === '/open' && event.request.method === 'POST') {
// Cache shared file temporarily
event.respondWith(handleShareTarget(event.request));
}
});Cache Cleanup:
- Files deleted immediately after opening in main app
- Stale files cleaned on service worker activation
Session Files:
-
{sessionName}.bin- Binary canvas data
- Current state
- Updated on save interval
-
{sessionName}.json- Chat history
- Metadata
- User information
-
{sessionName} {timestamp}.bin- Timestamped backups
- Created on each save
- Manual recovery if needed
File Format (Binary):
Canvas Data:
- Width: 2 bytes (uint16)
- Height: 2 bytes (uint16)
- Data: width * height * 2 bytes (character + attribute per cell)
Attributes Byte:
- Bits 0-3: Foreground color (0-15)
- Bits 4-7: Background color (0-7 or 0-15 with ICE)
- Bit 7: Blink (if not ICE colors)
All modules use the revealing module pattern:
const Module = (() => {
// Private variables
let privateVar = 0;
// Private functions
function privateFunc() {
// ...
}
// Public API
function publicFunc() {
// ...
}
return {
publicFunc: publicFunc,
};
})();Event-driven architecture for loose coupling:
// Publish
document.dispatchEvent(
new CustomEvent('onTextCanvasChange', {
detail: { x, y, char, fg, bg },
}),
);
// Subscribe
document.addEventListener('onTextCanvasChange', handler);Undo/redo system:
const command = {
execute: () => {
/* apply change */
},
undo: () => {
/* revert change */
},
};
State.textArtCanvas.startUndo(); // Push to undo stack
// ... make changes ...
State.textArtCanvas.endUndo(); // Finalize undo entryTool system allows runtime tool switching:
const tools = {
keyboard: keyboardTool,
freehand: freehandTool,
brush: brushTool,
// ...
};
function selectTool(toolName) {
currentTool.disable();
currentTool = tools[toolName];
currentTool.enable();
}The canvas rendering system employs multiple optimization strategies for handling canvases of any size efficiently.
Lazy Chunk Creation: Canvas is divided into 25-row chunks. Only visible chunks (plus a buffer) are created and rendered:
const chunkSize = 25;
const canvasChunks = new Map(); // chunkIndex → chunk data
let activeChunks = new Set(); // Currently visible chunks
// Create chunk only when needed
function getOrCreateCanvasChunk(chunkIndex) {
if (canvasChunks.has(chunkIndex)) {
return canvasChunks.get(chunkIndex);
}
// Create new chunk with canvas, offscreen canvas for blink
const chunk = {
canvas: createCanvas(width, height),
ctx: ...,
onBlinkCanvas: createCanvas(width, height),
offBlinkCanvas: createCanvas(width, height),
rendered: false
};
canvasChunks.set(chunkIndex, chunk);
return chunk;
}Viewport Tracking: Monitor scroll position to determine which chunks are visible:
const viewportState = {
scrollTop: 0,
scrollLeft: 0,
containerHeight: 0,
visibleStartRow: 0,
visibleEndRow: 0,
};
function calculateVisibleChunks() {
const viewportTop = viewportState.scrollTop;
const viewportBottom = viewportTop + viewportState.containerHeight;
const bufferZone = chunkSize * fontHeight; // 1 chunk buffer
const startChunk = Math.floor(
(viewportTop - bufferZone) / (chunkSize * fontHeight),
);
const endChunk = Math.floor(
(viewportBottom + bufferZone) / (chunkSize * fontHeight),
);
return { startChunk, endChunk };
}Dynamic Chunk Management: Chunks are attached/detached from DOM as user scrolls:
function renderVisibleChunks() {
const { startChunk, endChunk } = calculateVisibleChunks();
// Remove chunks outside viewport
activeChunks.forEach(chunkIndex => {
if (chunkIndex < startChunk || chunkIndex > endChunk) {
const chunk = canvasChunks.get(chunkIndex);
if (chunk.canvas.parentNode) {
canvasContainer.removeChild(chunk.canvas);
}
}
});
// Add visible chunks to DOM
for (let i = startChunk; i <= endChunk; i++) {
const chunk = getOrCreateCanvasChunk(i);
if (!chunk.canvas.parentNode) {
canvasContainer.appendChild(chunk.canvas);
activeChunks.add(i);
}
if (!chunk.rendered) {
renderChunk(chunk);
}
}
}Performance Impact:
- 80×200 canvas: ~60% faster initial load (80ms vs 200ms)
- Memory: ~66% reduction (only visible chunks in memory)
- Scalability: Handles canvases up to 2000+ rows efficiently
Only redraw cells that have changed, not the entire canvas:
const dirtyRegions = [];
function enqueueDirtyCell(x, y) {
dirtyRegions.push({ x, y, w: 1, h: 1 });
processDirtyRegions();
}
function processDirtyRegions() {
// Coalesce adjacent regions to minimize draw calls
const coalesced = coalesceRegions(dirtyRegions);
dirtyRegions = [];
coalesced.forEach(region => {
drawRegion(region.x, region.y, region.w, region.h);
});
}Virtualization-Aware Rendering: Dirty tracking respects chunk visibility:
function redrawGlyph(index, x, y) {
const chunkIndex = Math.floor(y / chunkSize);
const chunk = canvasChunks.get(chunkIndex);
if (!chunk || !activeChunks.has(chunkIndex)) {
// Mark chunk as dirty for later rendering
if (chunk) chunk.rendered = false;
return;
}
// Render immediately for visible chunks
redrawGlyphInChunk(index, x, y, chunk);
}RAF Throttling (RequestAnimationFrame Throttling) synchronizes expensive operations with the browser's rendering cycle (typically 60fps = 16.67ms per frame). cite
Scroll Event Throttling:
let scrollScheduled = false;
let pendingScrollUpdate = false;
function handleScroll() {
pendingScrollUpdate = true;
if (scrollScheduled) return;
scrollScheduled = true;
requestAnimationFrame(() => {
updateViewportState();
renderVisibleChunks();
scrollScheduled = false;
// Handle accumulated scroll events
if (pendingScrollUpdate) {
pendingScrollUpdate = false;
handleScroll();
}
});
}
viewportElement.addEventListener('scroll', handleScroll, { passive: true });Dirty Region Rendering:
let dirtyRegionScheduled = false;
function processDirtyRegions() {
if (dirtyRegions.length === 0 || processingDirtyRegions) return;
if (!dirtyRegionScheduled) {
dirtyRegionScheduled = true;
requestAnimationFrame(() => {
processingDirtyRegions = true;
const coalescedRegions = coalesceRegions(dirtyRegions);
dirtyRegions = [];
coalescedRegions.forEach(region => {
drawRegion(region.x, region.y, region.w, region.h);
});
processingDirtyRegions = false;
dirtyRegionScheduled = false;
});
}
}Benefits:
- Prevents redundant rendering within same frame
- Syncs updates with browser repaint cycle
- Eliminates jank and tearing
- Maintains smooth 60fps even during rapid changes
- Reduces CPU usage by batching operations
Progressive Rendering: For large canvases, render in batches across multiple frames:
function redrawEntireImage(onProgress, onComplete) {
const totalCells = rows * columns;
// Dynamic batch sizing
let batchSize;
if (totalCells < 10000) {
batchSize = 10; // Small: 10 rows per frame
} else if (totalCells < 50000) {
batchSize = 5; // Medium: 5 rows per frame
} else {
batchSize = 3; // Large: 3 rows per frame
}
function renderBatch(startRow) {
const endRow = Math.min(startRow + batchSize, rows);
drawRegion(0, startRow, columns, endRow - startRow);
if (onProgress) onProgress(endRow / rows);
if (endRow < rows) {
requestAnimationFrame(() => renderBatch(endRow));
} else if (onComplete) {
requestAnimationFrame(onComplete);
}
}
renderBatch(0);
}The blink effect uses selective cell tracking
const chunk = {
canvas: createCanvas(...),
ctx: canvas.getContext('2d'),
blinkCells: new Set(), // Track cells with blink attribute
startRow: ...,
endRow: ...
};
// During render, identify cells with blink attribute (background >= 8)
function redrawGlyphInChunk(index, x, y, chunk) {
// ...
if (!iceColors && isBlinkBackground) {
chunk.blinkCells.add(index); // Track for blink timer
background -= 8;
// Draw based on current blink state
State.font.draw(
charCode,
blinkOn ? background : foreground,
background,
chunk.ctx,
x,
localY
);
} else {
chunk.blinkCells.delete(index);
State.font.draw(charCode, foreground, background, chunk.ctx, x, localY);
}
}
// Blink timer only redraws tracked cells
function blink() {
blinkOn = !blinkOn;
activeChunks.forEach(chunkIndex => {
const chunk = canvasChunks.get(chunkIndex);
if (!chunk || chunk.blinkCells.size === 0) return;
// Only redraw cells that need blinking
chunk.blinkCells.forEach(index => {
// ...
State.font.draw(
charCode,
blinkOn ? background : foreground,
background,
chunk.ctx,
x,
localY
);
});
});
}Benefits:
- No offscreen canvases needed (reduced memory by 66%)
- Only redraws cells that actually blink
- Mutex-based timer prevents race conditions
- Automatically skips chunks with no blinking cells
Result: Smooth 60 FPS on canvases of any size (tested with 2000+ row canvases)
Lazy Loading: Fonts loaded on-demand, not all at once:
// Load font when first used
function loadFont(fontName) {
if (!fontCache.has(fontName)) {
return fetch(`/ui/fonts/${fontName}.png`).then(img =>
fontCache.set(fontName, img),
);
}
}Font Cache: Keep recently used fonts in memory:
const fontCache = new Map(); // LRU cache with size limitMessage Batching: Multiple canvas changes sent together:
const changes = [];
// ... collect changes ...
worker.postMessage({ cmd: 'draw', blocks: changes });Web Worker: WebSocket communication in worker thread keeps UI responsive:
// Main thread
const worker = new Worker('websocket.js');
// First message MUST be init to establish security context
worker.postMessage({ cmd: 'init' });
// Worker thread
self.onmessage = e => {
const { cmd, data } = e.data;
// Handle WebSocket communication
};Security Features:
- Mandatory Initialization: Worker requires
initcommand as first message to establish security context - Trusted URL Construction: WebSocket URLs are constructed only from the worker's own
locationobject (protocol, hostname, port) - URL Validation: Malformed WebSocket URLs are detected and rejected using URL constructor validation
- Input Sanitization: All error messages and unknown commands sanitize output to prevent injection
- JSON Parsing Protection: Invalid JSON from server is caught and safely logged without crashing
- Silent Connection Check: Server availability is tested silently before prompting user, avoiding intrusive errors
Connection Flow:
// 1. Initialize worker with security context
worker.postMessage({ cmd: 'init' });
// 2. Worker establishes trusted parameters from its own location
// allowedHostname = self.location.hostname
// trustedProtocol = self.location.protocol === 'https:' ? 'wss:' : 'ws:'
// trustedPort = self.location.port || (https ? '443' : '80')
// 3. Silent connection check (optional)
worker.postMessage({ cmd: 'connect', silentCheck: true });
// 4. User chooses collaboration or local mode
// 5. Full connection established if user opts inCompression: Run-length encoding reduces storage size:
// Before: [1,1,1,1,1,2,2,2,3,3]
// After: [[1,5],[2,3],[3,2]]
// Savings: 60% typicalDebouncing: Avoid excessive saves:
const debouncedSave = debounce(saveToIndexedDB, 500);- Client Editor Manual - Visual guide to the Frontend text art editor
- Collaboration Server - Server setup and protocol
- Building and Developing - Build process
- Testing - Test architecture
- CI/CD Pipeline - Deployment architecture