-
-
Notifications
You must be signed in to change notification settings - Fork 3
architecture
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