Skip to content

architecture

xero edited this page Nov 21, 2025 · 9 revisions

Architecture

This document provides a comprehensive overview of the teXt0wnz architecture, including application structure, data flow, module organization, and key design decisions.

Table of Contents

High-Level Overview

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:

  1. Client-only mode - Standalone editor with local storage
  2. 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)    β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Application Modes

Client-Only Mode (Standalone)

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

Collaborative Mode

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

Client Architecture

Layer Structure

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                   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)   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Core Modules

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

Event System

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)

Tool Pattern

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

Server Architecture

Express Server Structure

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                   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 β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Server Modules

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

Message Protocol

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 toggle

Server 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 resize

Chat 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

State Synchronization

When a user joins:

  1. Server sends current canvas state via "start" message
  2. Client applies canvas settings (size, font, colors, spacing)
  3. Client renders canvas from imageData
  4. User is added to session list

When a drawing occurs:

  1. Client sends "draw" message with affected blocks
  2. Server updates internal imageData
  3. Server broadcasts to all other clients
  4. Other clients update their canvas

Data Flow

Drawing Action Flow

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 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    β”‚
                   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

File Save Flow

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ User Clicks β”‚ (Save button)
β”‚    Save     β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Get Canvas β”‚ (State.textArtCanvas)
β”‚    Data     β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Format    β”‚ (ANSI, BIN, XBIN, etc.)
β”‚  Converter  β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚    Add      β”‚ (if enabled)
β”‚   SAUCE     β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Download   β”‚ (Browser download)
β”‚    File     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Auto-Save Flow (Client-Only)

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Canvas    β”‚ (change detected)
β”‚   Changes   β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Compress   β”‚ (RLE compression)
β”‚    Data     β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Save to   β”‚ (with debouncing)
β”‚  IndexedDB  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Module Structure

Client Modules

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

Server Modules

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

Dependency Graph (Client)

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

Build System

Vite Configuration

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)

Asset Optimization

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

Storage and Persistence

Client Storage (IndexedDB)

Database: textArtDB

Object Stores:

  1. canvasData

    • Key: currentCanvas
    • Value: Compressed canvas data
    • Updates: Debounced (500ms after last change)
  2. 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);

Service Worker Cache (Cache API)

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

Server Storage (File System)

Session Files:

  1. {sessionName}.bin

    • Binary canvas data
    • Current state
    • Updated on save interval
  2. {sessionName}.json

    • Chat history
    • Metadata
    • User information
  3. {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)

Design Patterns

Module Pattern

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,
	};
})();

Observer Pattern

Event-driven architecture for loose coupling:

// Publish
document.dispatchEvent(
	new CustomEvent('onTextCanvasChange', {
		detail: { x, y, char, fg, bg },
	}),
);

// Subscribe
document.addEventListener('onTextCanvasChange', handler);

Command Pattern

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 entry

Strategy Pattern

Tool system allows runtime tool switching:

const tools = {
	keyboard: keyboardTool,
	freehand: freehandTool,
	brush: brushTool,
	// ...
};

function selectTool(toolName) {
	currentTool.disable();
	currentTool = tools[toolName];
	currentTool.enable();
}

Performance Optimizations

Canvas Rendering

The canvas rendering system employs multiple optimization strategies for handling canvases of any size efficiently.

Virtualized Viewport Rendering

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

Dirty Region Tracking

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);
}

RequestAnimationFrame Throttling

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);
}

Optimized Blink Effect

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)

Font Loading

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 limit

Network Optimization

Message 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 init command as first message to establish security context
  • Trusted URL Construction: WebSocket URLs are constructed only from the worker's own location object (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 in

Storage Optimization

Compression: 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% typical

Debouncing: Avoid excessive saves:

const debouncedSave = debounce(saveToIndexedDB, 500);

Related Documentation

Clone this wiki locally