UNPKG

@ertekinno/human-like

Version:

A sophisticated React typewriter effect library with realistic human typing behavior, mobile/desktop keyboards, and comprehensive theming support

1,954 lines 72 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); import { useState, useRef, useEffect, useCallback } from "react"; const TIMING_CONSTANTS = { BASE_SPEED: 80, // Average milliseconds per character SPEED_VARIATION: 40, // Random timing variation (±40ms) MIN_CHAR_DELAY: 25, // Minimum delay between characters // Punctuation and Structure SENTENCE_PAUSE: 500, // Pause after . ! ? COMMA_PAUSE: 200, // Pause after , ; : WORD_SPACE: 150, // Pause between words LINE_BREAK: 800, // Pause for new paragraphs // Mistake Handling REALIZATION_DELAY: 300, // Time to notice mistake CORRECTION_PAUSE: 250, // Pause before retyping BACKSPACE_SPEED: 60, // Speed of corrections // Human Behavior THINKING_PAUSE: 400, // Pause before complex words FATIGUE_INCREMENT: 0.5, // Gradual slowdown per 100 chars BURST_SPEED_MULTIPLIER: 0.6, // Speed multiplier during bursts CONCENTRATION_PAUSE: 800, // Duration of concentration lapses // Enhanced Realism SHIFT_HESITATION: 100, // Extra delay for shift key characters (increased) CAPS_LOCK_ON_DELAY: 150, // Delay when turning CAPS LOCK on CAPS_LOCK_OFF_DELAY: 100, // Delay when turning CAPS LOCK off CAPS_SEQUENCE_THRESHOLD: 3, // Min consecutive caps to trigger CAPS LOCK mode NUMBER_ROW_PENALTY: 35, // Extra delay for number characters SYMBOL_BASE_PENALTY: 25, // Base extra delay for symbols LOOK_AHEAD_CHANCE: 0.08 // 8% chance of look-ahead typing }; const BEHAVIOR_RATES = { MISTAKE_FREQUENCY: 0.03, // 3% base mistake rate CONCENTRATION_LAPSE: 0.03, // 3% random pause chance BURST_TYPING: 0.15, // 15% rapid sequence chance FATIGUE_FACTOR: 1e-4, // Gradual slowdown rate OVERCORRECTION_RATE: 0.2 // 20% chance of making mistake while correcting }; const DESKTOP_ADJACENT = { "q": ["w", "a", "s"], "w": ["q", "e", "a", "s", "d"], "e": ["w", "r", "s", "d", "f"], "r": ["e", "t", "d", "f", "g"], "t": ["r", "y", "f", "g", "h"], "y": ["t", "u", "g", "h", "j"], "u": ["y", "i", "h", "j", "k"], "i": ["u", "o", "j", "k", "l"], "o": ["i", "p", "k", "l", ";"], "p": ["o", "[", "l", ";", "'"], "[": ["p", "]", ";", "'"], "]": ["[", "\\", "'"], "a": ["q", "w", "s", "z"], "s": ["q", "w", "e", "a", "d", "z", "x"], "d": ["w", "e", "r", "s", "f", "x", "c"], "f": ["e", "r", "t", "d", "g", "c", "v"], "g": ["r", "t", "y", "f", "h", "v", "b"], "h": ["t", "y", "u", "g", "j", "b", "n"], "j": ["y", "u", "i", "h", "k", "n", "m"], "k": ["u", "i", "o", "j", "l", "m", ","], "l": ["i", "o", "p", "k", ";", ",", "."], ";": ["o", "p", "[", "l", "'", ".", "/"], "'": ["p", "[", "]", ";", "/", "."], "z": ["a", "s", "x"], "x": ["z", "s", "d", "c"], "c": ["x", "d", "f", "v", " "], "v": ["c", "f", "g", "b", " "], "b": ["v", "g", "h", "n", " "], "n": ["b", "h", "j", "m", " "], "m": ["n", "j", "k", ",", " "], ",": ["m", "k", "l", "."], ".": [",", "l", ";", "/", "'"], "/": [".", ";", "'"], " ": ["c", "v", "b", "n", "m"] // Space bar adjacent to bottom row }; const MOBILE_ADJACENT = { // Top row - mobile keyboards often have tighter spacing "q": ["w", "a", "s"], // Less diagonal mistakes on mobile "w": ["q", "e", "s", "a"], // More focused on immediate neighbors "e": ["w", "r", "d", "s"], "r": ["e", "t", "f", "d"], "t": ["r", "y", "g", "f"], "y": ["t", "u", "h", "g"], "u": ["y", "i", "j", "h"], "i": ["u", "o", "k", "j"], "o": ["i", "p", "l", "k"], "p": ["o", "l"], // Middle row - more fat finger mistakes due to touch "a": ["q", "w", "s", "z"], "s": ["a", "d", "w", "e", "z", "x"], // High mistake area on mobile "d": ["s", "f", "e", "r", "x", "c"], "f": ["d", "g", "r", "t", "c", "v"], "g": ["f", "h", "t", "y", "v", "b"], "h": ["g", "j", "y", "u", "b", "n"], "j": ["h", "k", "u", "i", "n", "m"], "k": ["j", "l", "i", "o", "m"], "l": ["k", "o", "p"], // Bottom row - spacebar interference more common on mobile "z": ["a", "s", "x"], "x": ["z", "c", "s", "d"], "c": ["x", "v", "d", "f", " "], // Space bar mistakes more common "v": ["c", "b", "f", "g", " "], "b": ["v", "n", "g", "h", " "], "n": ["b", "m", "h", "j", " "], "m": ["n", "j", "k", " "], // Space bar - different pattern on mobile due to wider space key " ": ["c", "v", "b", "n", "m", "x", "z"], // Includes more bottom row keys // Numbers often above letters on mobile, but closer spacing "1": ["2", "q", "w"], "2": ["1", "3", "q", "w", "e"], "3": ["2", "4", "w", "e", "r"], "4": ["3", "5", "e", "r", "t"], "5": ["4", "6", "r", "t", "y"], "6": ["5", "7", "t", "y", "u"], "7": ["6", "8", "y", "u", "i"], "8": ["7", "9", "u", "i", "o"], "9": ["8", "0", "i", "o", "p"], "0": ["9", "o", "p"] }; const QWERTY_ADJACENT = DESKTOP_ADJACENT; function getAdjacentKeys(keyboardMode) { return keyboardMode === "mobile" ? MOBILE_ADJACENT : DESKTOP_ADJACENT; } const COMMON_WORDS = /* @__PURE__ */ new Set([ "the", "and", "for", "are", "but", "not", "you", "all", "can", "had", "her", "was", "one", "our", "out", "day", "get", "has", "him", "his", "how", "man", "new", "now", "old", "see", "two", "way", "who", "boy", "did", "its", "let", "put", "say", "she", "too", "use", "that", "with", "have", "this", "will", "your", "from", "they", "know", "want", "been", "good", "much", "some", "time", "very", "when", "come", "here", "just", "like", "long", "make", "many", "over", "such", "take", "than", "them", "well", "were" ]); const COMMON_TYPOS = { "the": "teh", "and": "adn", "for": "fro", "you": "yuo", "that": "taht", "this": "tihs", "with": "wiht", "have": "ahve", "from": "form", "they": "thye", "been": "bene", "than": "htan", "what": "waht", "your": "yuor", "when": "wehn", "there": "tehre", "their": "thier", "would": "woudl", "could": "coudl", "should": "shoudl", "through": "trhough", "because": "becasue", "before": "beofre", "after": "aftre", "where": "whree", "which": "whihc", "between": "betwene", "different": "differnet", "important": "importnat", "example": "exmaple", "without": "withuot", "another": "antoher", "development": "developement", "environment": "enviroment", "government": "goverment", "management": "managment", "information": "infromation", "available": "availabe", "business": "buisness", "complete": "compelte", "language": "langauge", "experience": "experiance", "position": "postion", "question": "quesiton", "remember": "remeber", "separate": "seperate", "something": "somehting", "together": "togehter", "understand": "udnerstand" }; const SPECIAL_CHARS = /* @__PURE__ */ new Set([ "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "-", "_", "+", "=", "[", "]", "{", "}", "\\", "|", ";", ":", "'", '"', ",", ".", "<", ">", "/", "?", "`", "~" ]); const SHIFT_CHARS = /* @__PURE__ */ new Set([ "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "_", "+", "{", "}", "|", ":", '"', "<", ">", "?", "~" ]); const NUMBER_CHARS = /* @__PURE__ */ new Set(["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]); const SYMBOL_COMPLEXITY = { // Simple punctuation ".": 1, ",": 1, "?": 1, "!": 1, ";": 1, ":": 1, "'": 1, '"': 1, // Medium complexity "-": 2, "_": 2, "(": 2, ")": 2, "[": 2, "]": 2, "/": 2, // Complex symbols requiring precise finger movement "@": 3, "#": 3, "$": 3, "%": 3, "^": 3, "&": 3, "*": 3, "+": 3, "=": 3, "{": 3, "}": 3, "\\": 3, "|": 3, "`": 3, "~": 3, "<": 3, ">": 3 }; const SENTENCE_ENDINGS = /* @__PURE__ */ new Set([".", "!", "?"]); const CLAUSE_SEPARATORS = /* @__PURE__ */ new Set([",", ";", ":"]); const LINE_BREAK_CHARS = /* @__PURE__ */ new Set(["\n", "\r\n", "\r"]); const DEFAULT_CONFIG = { speed: TIMING_CONSTANTS.BASE_SPEED, speedVariation: TIMING_CONSTANTS.SPEED_VARIATION, mistakeFrequency: BEHAVIOR_RATES.MISTAKE_FREQUENCY, mistakeTypes: { adjacent: true, random: false, doubleChar: true, commonTypos: true }, fatigueEffect: true, concentrationLapses: true, overcorrection: true, debug: false, sentencePause: TIMING_CONSTANTS.SENTENCE_PAUSE, wordPause: TIMING_CONSTANTS.WORD_SPACE, thinkingPause: TIMING_CONSTANTS.THINKING_PAUSE, minCharDelay: TIMING_CONSTANTS.MIN_CHAR_DELAY, backspaceSpeed: TIMING_CONSTANTS.BACKSPACE_SPEED, realizationDelay: TIMING_CONSTANTS.REALIZATION_DELAY, correctionPause: TIMING_CONSTANTS.CORRECTION_PAUSE, // Keyboard simulation defaults keyboardMode: "mobile" }; const MOBILE_LAYOUT = { views: { letters: [ "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "a", "s", "d", "f", "g", "h", "j", "k", "l", "z", "x", "c", "v", "b", "n", "m" ], numbers: [ "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "/", ":", ";", "(", ")", "$", "&", "@", '"', ".", ",", "?", "!", "'", "[", "]", "{", "}", "#", "%", "^", "*", "+", "=", "_", "\\", "|", "~", "<", ">", "€", "£", "¥", "•", "..." ], symbols: [ "[", "]", "{", "}", "#", "%", "^", "*", "+", "=", "_", "\\", "|", "~", "<", ">", "€", "£", "¥", "•", ".", ",", "?", "!", "'", '"', "/", ":", ";", "(", ")", "$", "&", "@", "`", "§", "¿", "¡", "«", "»", "°", "†", "‡", "…", "‰", "′", "″", "‹", "›" ], emoji: [ "😀", "😃", "😄", "😁", "😆", "😅", "🤣", "😂", "🙂", "🙃", "😉", "😊", "😇", "🥰", "😍", "🤩", "😘", "😗", "☺", "😚", "😙", "🥲", "😋", "😛", "😜", "🤪", "😝", "🤑", "🤗", "🤭" ] }, viewSwitchers: { toNumbers: "123", toSymbols: "#+=", toLetters: "ABC", toEmoji: "😀" }, modifiers: { shift: "⇧", caps: "CAPS", space: "space", enter: "return", backspace: "⌫" }, keyDurations: { letter: 80, // Fast letter typing number: 90, // Slightly slower for numbers symbol: 100, // Slower for symbols modifier: 120, // Shift/caps key press viewSwitch: 110, // View switching (123, ABC, etc.) space: 75, // Space bar enter: 90, // Return key backspace: 110 // Backspace key } }; ({ ...MOBILE_LAYOUT }); ({ ...MOBILE_LAYOUT }); const MOBILE_CHARACTER_TO_VIEW = { // Letters (lowercase and uppercase) ...Object.fromEntries("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".split("").map((c) => [c, "letters"])), // Numbers and basic symbols on number view "1": "numbers", "2": "numbers", "3": "numbers", "4": "numbers", "5": "numbers", "6": "numbers", "7": "numbers", "8": "numbers", "9": "numbers", "0": "numbers", "-": "numbers", "/": "numbers", ":": "numbers", ";": "numbers", "(": "numbers", ")": "numbers", "$": "numbers", "&": "numbers", "@": "numbers", '"': "numbers", ".": "numbers", ",": "numbers", "?": "numbers", "!": "numbers", "'": "numbers", // Complex symbols on symbol view "[": "symbols", "]": "symbols", "{": "symbols", "}": "symbols", "#": "symbols", "%": "symbols", "^": "symbols", "*": "symbols", "+": "symbols", "=": "symbols", "_": "symbols", "\\": "symbols", "|": "symbols", "~": "symbols", "<": "symbols", ">": "symbols", "€": "symbols", "£": "symbols", "¥": "symbols", "•": "symbols", "`": "symbols", "§": "symbols", "¿": "symbols", "¡": "symbols", "«": "symbols", "»": "symbols", "°": "symbols", "†": "symbols", "‡": "symbols", "…": "symbols", "‰": "symbols", "′": "symbols", "″": "symbols", "‹": "symbols", "›": "symbols" }; const DESKTOP_QWERTY_LAYOUT = { views: { letters: [ "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "a", "s", "d", "f", "g", "h", "j", "k", "l", "z", "x", "c", "v", "b", "n", "m" ], numbers: ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"], symbols: [ "`", "~", "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "-", "_", "+", "=", "[", "]", "{", "}", "\\", "|", ";", ":", "'", '"', ",", ".", "<", ">", "/", "?" ] }, viewSwitchers: { toNumbers: "numbers", // No view switching on desktop toSymbols: "symbols", // Symbols accessed via shift toLetters: "letters" }, modifiers: { shift: "shift", caps: "caps lock", space: "space", enter: "enter", backspace: "backspace" }, keyDurations: { letter: 70, // Fast letter typing on desktop number: 85, // Numbers require reaching up symbol: 95, // Symbols often need shift modifier: 90, // Modifier key press viewSwitch: 0, // No view switching on desktop space: 60, // Space bar enter: 80, // Enter key backspace: 100 // Backspace key } }; const DESKTOP_KEY_MAPPING = { // Letters (lowercase) "a": ["a", false], "b": ["b", false], "c": ["c", false], "d": ["d", false], "e": ["e", false], "f": ["f", false], "g": ["g", false], "h": ["h", false], "i": ["i", false], "j": ["j", false], "k": ["k", false], "l": ["l", false], "m": ["m", false], "n": ["n", false], "o": ["o", false], "p": ["p", false], "q": ["q", false], "r": ["r", false], "s": ["s", false], "t": ["t", false], "u": ["u", false], "v": ["v", false], "w": ["w", false], "x": ["x", false], "y": ["y", false], "z": ["z", false], // Letters (uppercase) - same key + shift "A": ["a", true], "B": ["b", true], "C": ["c", true], "D": ["d", true], "E": ["e", true], "F": ["f", true], "G": ["g", true], "H": ["h", true], "I": ["i", true], "J": ["j", true], "K": ["k", true], "L": ["l", true], "M": ["m", true], "N": ["n", true], "O": ["o", true], "P": ["p", true], "Q": ["q", true], "R": ["r", true], "S": ["s", true], "T": ["t", true], "U": ["u", true], "V": ["v", true], "W": ["w", true], "X": ["x", true], "Y": ["y", true], "Z": ["z", true], // Numbers (no shift) "1": ["1", false], "2": ["2", false], "3": ["3", false], "4": ["4", false], "5": ["5", false], "6": ["6", false], "7": ["7", false], "8": ["8", false], "9": ["9", false], "0": ["0", false], // Numbers with shift (top row symbols) "!": ["1", true], "@": ["2", true], "#": ["3", true], "$": ["4", true], "%": ["5", true], "^": ["6", true], "&": ["7", true], "*": ["8", true], "(": ["9", true], ")": ["0", true], // Other keys without shift "`": ["`", false], "-": ["-", false], "=": ["=", false], "[": ["[", false], "]": ["]", false], "\\": ["\\", false], ";": [";", false], "'": ["'", false], ",": [",", false], ".": [".", false], "/": ["/", false], " ": ["space", false], "\n": ["enter", false], " ": ["tab", false], // Other keys with shift "~": ["`", true], "_": ["-", true], "+": ["=", true], "{": ["[", true], "}": ["]", true], "|": ["\\", true], ":": [";", true], '"': ["'", true], "<": [",", true], ">": [".", true], "?": ["/", true] }; const MOBILE_TIMING_PROFILES = { // Standard smartphone typing MOBILE_CASUAL: { name: "Mobile Casual", description: "Average smartphone user, thumb typing", baseMultiplier: 1, keyDurations: { letter: 120, number: 140, symbol: 160, modifier: 180, viewSwitch: 150, space: 100, enter: 130, backspace: 150 }, viewSwitchDelay: 50, consecutiveKeyBonus: 0.9, complexSymbolPenalty: 40, capsLockTransitionDelay: 60 }, // Fast mobile typist MOBILE_FAST: { name: "Mobile Fast", description: "Experienced mobile user, swipe/predictive text habits", baseMultiplier: 0.7, keyDurations: { letter: 80, number: 95, symbol: 110, modifier: 120, viewSwitch: 100, space: 70, enter: 90, backspace: 110 }, viewSwitchDelay: 30, consecutiveKeyBonus: 0.85, complexSymbolPenalty: 25, capsLockTransitionDelay: 40 }, // Slow/careful mobile typing MOBILE_CAREFUL: { name: "Mobile Careful", description: "Deliberate mobile typing, hunt-and-peck style", baseMultiplier: 1.5, keyDurations: { letter: 180, number: 220, symbol: 280, modifier: 250, viewSwitch: 200, space: 150, enter: 180, backspace: 220 }, viewSwitchDelay: 80, consecutiveKeyBonus: 0.95, complexSymbolPenalty: 60, capsLockTransitionDelay: 100 }, // Tablet typing (larger screen, different hand position) TABLET: { name: "Tablet", description: "Tablet device, often landscape mode with more fingers", baseMultiplier: 0.8, keyDurations: { letter: 90, number: 110, symbol: 130, modifier: 140, viewSwitch: 120, space: 80, enter: 100, backspace: 130 }, viewSwitchDelay: 40, consecutiveKeyBonus: 0.8, // More fingers = better hand alternation complexSymbolPenalty: 30, capsLockTransitionDelay: 50 } }; const DESKTOP_TIMING_PROFILES = { // Standard desktop typing DESKTOP_AVERAGE: { name: "Desktop Average", description: "Average desktop user, ~40 WPM", baseMultiplier: 1, keyDurations: { letter: 80, number: 100, symbol: 120, modifier: 90, viewSwitch: 0, // No view switching on desktop space: 70, enter: 90, backspace: 100 }, viewSwitchDelay: 0, consecutiveKeyBonus: 0.85, // Good hand alternation on QWERTY complexSymbolPenalty: 30, capsLockTransitionDelay: 50 }, // Fast desktop typist DESKTOP_FAST: { name: "Desktop Fast", description: "Experienced typist, ~70+ WPM, touch typing", baseMultiplier: 0.6, keyDurations: { letter: 50, number: 65, symbol: 80, modifier: 60, viewSwitch: 0, space: 45, enter: 60, backspace: 70 }, viewSwitchDelay: 0, consecutiveKeyBonus: 0.8, complexSymbolPenalty: 15, capsLockTransitionDelay: 25 }, // Programming-focused typing DESKTOP_PROGRAMMER: { name: "Desktop Programmer", description: "Developer typing, frequent symbols and modifiers", baseMultiplier: 0.7, keyDurations: { letter: 60, number: 70, symbol: 75, // Programmers are fast with symbols modifier: 65, viewSwitch: 0, space: 50, enter: 70, backspace: 80 }, viewSwitchDelay: 0, consecutiveKeyBonus: 0.8, complexSymbolPenalty: 10, // Less penalty for complex symbols capsLockTransitionDelay: 30 }, // Hunt-and-peck desktop typing DESKTOP_SLOW: { name: "Desktop Slow", description: "Hunt-and-peck typing, looking at keyboard", baseMultiplier: 2, keyDurations: { letter: 160, number: 200, symbol: 250, modifier: 180, viewSwitch: 0, space: 140, enter: 160, backspace: 180 }, viewSwitchDelay: 0, consecutiveKeyBonus: 0.95, // Less benefit from hand alternation complexSymbolPenalty: 80, capsLockTransitionDelay: 120 }, // Gaming keyboard (mechanical, fast response) DESKTOP_GAMING: { name: "Desktop Gaming", description: "Mechanical keyboard, gaming-optimized typing", baseMultiplier: 0.5, keyDurations: { letter: 40, number: 50, symbol: 60, modifier: 45, viewSwitch: 0, space: 35, enter: 50, backspace: 60 }, viewSwitchDelay: 0, consecutiveKeyBonus: 0.75, complexSymbolPenalty: 10, capsLockTransitionDelay: 20 } }; function getDefaultTimingProfile(keyboardMode, userHint) { if (keyboardMode === "mobile") { if (userHint === "fast") return MOBILE_TIMING_PROFILES.MOBILE_FAST; if (userHint === "slow" || userHint === "careful") return MOBILE_TIMING_PROFILES.MOBILE_CAREFUL; if (userHint === "tablet") return MOBILE_TIMING_PROFILES.TABLET; return MOBILE_TIMING_PROFILES.MOBILE_CASUAL; } else { if (userHint === "fast") return DESKTOP_TIMING_PROFILES.DESKTOP_FAST; if (userHint === "slow") return DESKTOP_TIMING_PROFILES.DESKTOP_SLOW; if (userHint === "programmer" || userHint === "developer") return DESKTOP_TIMING_PROFILES.DESKTOP_PROGRAMMER; if (userHint === "gaming") return DESKTOP_TIMING_PROFILES.DESKTOP_GAMING; return DESKTOP_TIMING_PROFILES.DESKTOP_AVERAGE; } } function applyTimingProfile(layout, profile) { return { ...layout, keyDurations: { ...profile.keyDurations } }; } function calculateContextualTiming(baseDelay, profile, context) { let adjustedDelay = baseDelay * profile.baseMultiplier; if (context.isConsecutiveSameHand) { adjustedDelay *= profile.consecutiveKeyBonus; } if (context.isComplexSymbol) { adjustedDelay += profile.complexSymbolPenalty; } if (context.isViewSwitch) { adjustedDelay += profile.viewSwitchDelay; } if (context.isCapsLockTransition) { adjustedDelay += profile.capsLockTransitionDelay; } return adjustedDelay; } class KeyboardAnalyzer { constructor(config = {}) { __publicField(this, "config"); __publicField(this, "layout"); __publicField(this, "state"); __publicField(this, "timingProfile"); this.config = { keyboardMode: "mobile", capsLockThreshold: 3, useNaturalTiming: true, debug: false, ...config }; this.timingProfile = getDefaultTimingProfile(this.config.keyboardMode, this.config.typingSpeed); const baseLayout = config.customLayout || (this.config.keyboardMode === "mobile" ? MOBILE_LAYOUT : DESKTOP_QWERTY_LAYOUT); this.layout = applyTimingProfile(baseLayout, this.timingProfile); this.state = { currentView: "letters", capsLockActive: false, shiftActive: false, recentCharacters: [], mode: this.config.keyboardMode }; this.debug("KeyboardAnalyzer initialized", { config: this.config, timingProfile: this.timingProfile.name, layout: this.layout.viewSwitchers }); } /** * Analyzes a character and returns the key sequence needed to type it */ analyzeCharacter(character, charIndex, fullText) { this.debug(`Analyzing character: "${character}" at index ${charIndex}`); this.updateRecentCharacters(character); const capsLockInfo = this.analyzeCapsLockSequence(character, charIndex, fullText); let keys = []; if (this.config.keyboardMode === "mobile") { keys = this.analyzeMobileCharacter(character, capsLockInfo); } else { keys = this.analyzeDesktopCharacter(character, capsLockInfo); } const totalDuration = keys.reduce((sum, key) => sum + key.duration, 0); const sequence = { character, keys, totalDuration, usesCapsLock: capsLockInfo.isCapsLockSequence }; this.debug(`Generated sequence for "${character}":`, sequence); return sequence; } /** * Analyzes mobile keyboard character input */ analyzeMobileCharacter(character, capsInfo) { const keys = []; let sequenceIndex = 0; if (character === " ") { return [{ key: this.layout.modifiers.space, character, type: "space", keyboardView: this.state.currentView, isCapsLock: false, duration: this.layout.keyDurations.space, sequenceIndex: 0, sequenceLength: 1 }]; } if (character === "\n") { return [{ key: this.layout.modifiers.enter, character, type: "enter", keyboardView: this.state.currentView, isCapsLock: false, duration: this.layout.keyDurations.enter, sequenceIndex: 0, sequenceLength: 1 }]; } const targetView = this.getCharacterView(character); if (this.state.currentView !== targetView) { const viewSwitchKeys = this.getViewSwitchSequence(this.state.currentView, targetView); viewSwitchKeys.forEach((viewKey) => { keys.push({ ...viewKey, sequenceIndex: sequenceIndex++, sequenceLength: 0 // Will be updated after we know total length }); }); this.state.currentView = targetView; } if (character.match(/[A-Z]/)) { if (capsInfo.isCapsLockSequence) { if (capsInfo.isFirst) { keys.push(this.createCapsKey(true, sequenceIndex++)); this.state.capsLockActive = true; } else if (capsInfo.isLast) { keys.push(this.createLetterKey(character.toLowerCase(), sequenceIndex++, capsInfo.isCapsLockSequence)); keys.push(this.createCapsKey(false, sequenceIndex++)); this.state.capsLockActive = false; keys.forEach((key) => key.sequenceLength = keys.length); return keys; } } else { keys.push(this.createShiftKey(sequenceIndex++)); } } const mainKey = this.createCharacterKey(character, sequenceIndex++, capsInfo.isCapsLockSequence); keys.push(mainKey); keys.forEach((key) => key.sequenceLength = keys.length); return keys; } /** * Analyzes desktop keyboard character input */ analyzeDesktopCharacter(character, capsInfo) { const keys = []; let sequenceIndex = 0; if (character === " ") { return [{ key: "space", character, type: "space", keyboardView: "letters", isCapsLock: false, duration: this.layout.keyDurations.space, sequenceIndex: 0, sequenceLength: 1 }]; } if (character === "\n") { return [{ key: "enter", character, type: "enter", keyboardView: "letters", isCapsLock: false, duration: this.layout.keyDurations.enter, sequenceIndex: 0, sequenceLength: 1 }]; } const mapping = DESKTOP_KEY_MAPPING[character]; if (!mapping) { this.debug(`No desktop mapping found for character: "${character}"`); return [{ key: character.toLowerCase(), character, type: "letter", keyboardView: "letters", isCapsLock: false, duration: this.layout.keyDurations.letter, sequenceIndex: 0, sequenceLength: 1 }]; } const [physicalKey, requiresShift] = mapping; if (character.match(/[A-Z]/)) { if (capsInfo.isCapsLockSequence) { if (capsInfo.isFirst) { keys.push({ key: "caps lock", character, type: "modifier", keyboardView: "letters", isCapsLock: true, duration: this.layout.keyDurations.modifier, sequenceIndex: sequenceIndex++, sequenceLength: 0 }); } else if (capsInfo.isLast) { keys.push({ key: physicalKey, character, type: "letter", keyboardView: "letters", isCapsLock: true, duration: this.layout.keyDurations.letter, sequenceIndex: sequenceIndex++, sequenceLength: 0 }); keys.push({ key: "caps lock", character, type: "modifier", keyboardView: "letters", isCapsLock: true, duration: this.layout.keyDurations.modifier, sequenceIndex: sequenceIndex++, sequenceLength: 0 }); keys.forEach((key) => key.sequenceLength = keys.length); return keys; } } else if (requiresShift) { keys.push({ key: "shift", character, type: "modifier", keyboardView: "letters", isCapsLock: false, duration: this.layout.keyDurations.modifier, sequenceIndex: sequenceIndex++, sequenceLength: 0 }); } } else if (requiresShift) { keys.push({ key: "shift", character, type: "modifier", keyboardView: "symbols", isCapsLock: false, duration: this.layout.keyDurations.modifier, sequenceIndex: sequenceIndex++, sequenceLength: 0 }); } const keyType = character.match(/[a-zA-Z]/) ? "letter" : character.match(/[0-9]/) ? "number" : "symbol"; keys.push({ key: physicalKey, character, type: keyType, keyboardView: keyType === "letter" ? "letters" : keyType === "number" ? "numbers" : "symbols", isCapsLock: capsInfo.isCapsLockSequence, duration: this.layout.keyDurations[keyType], sequenceIndex: sequenceIndex++, sequenceLength: 0 }); keys.forEach((key) => key.sequenceLength = keys.length); return keys; } /** * Determines which keyboard view a character belongs to (mobile) */ getCharacterView(character) { if (this.config.keyboardMode === "desktop") { return "letters"; } return MOBILE_CHARACTER_TO_VIEW[character] || "letters"; } /** * Gets the sequence of view switch keys needed */ getViewSwitchSequence(from, to) { if (from === to) return []; const keys = []; switch (to) { case "numbers": keys.push({ key: this.layout.viewSwitchers.toNumbers, character: "", type: "view-switch", keyboardView: from, isCapsLock: false, duration: this.layout.keyDurations.viewSwitch, sequenceIndex: 0, sequenceLength: 0 }); break; case "symbols": if (from === "letters") { keys.push({ key: this.layout.viewSwitchers.toNumbers, character: "", type: "view-switch", keyboardView: from, isCapsLock: false, duration: this.layout.keyDurations.viewSwitch, sequenceIndex: 0, sequenceLength: 0 }); } keys.push({ key: this.layout.viewSwitchers.toSymbols, character: "", type: "view-switch", keyboardView: "numbers", isCapsLock: false, duration: this.layout.keyDurations.viewSwitch, sequenceIndex: 0, sequenceLength: 0 }); break; case "letters": keys.push({ key: this.layout.viewSwitchers.toLetters, character: "", type: "view-switch", keyboardView: from, isCapsLock: false, duration: this.layout.keyDurations.viewSwitch, sequenceIndex: 0, sequenceLength: 0 }); break; } return keys; } /** * Creates a caps lock key press */ createCapsKey(turningOn, sequenceIndex) { const baseDuration = this.layout.keyDurations.modifier; const contextualDuration = calculateContextualTiming(baseDuration, this.timingProfile, { isCapsLockTransition: true }); return { key: this.layout.modifiers.caps, character: "", type: "modifier", keyboardView: this.state.currentView, isCapsLock: true, duration: turningOn ? contextualDuration + 20 : contextualDuration, // Slightly longer for turning on sequenceIndex, sequenceLength: 0 }; } /** * Creates a shift key press */ createShiftKey(sequenceIndex) { return { key: this.layout.modifiers.shift, character: "", type: "modifier", keyboardView: this.state.currentView, isCapsLock: false, duration: this.layout.keyDurations.modifier, sequenceIndex, sequenceLength: 0 }; } /** * Creates a letter key press */ createLetterKey(letter, sequenceIndex, isCapsLock) { return { key: letter, character: letter, type: "letter", keyboardView: "letters", isCapsLock, duration: this.layout.keyDurations.letter, sequenceIndex, sequenceLength: 0 }; } /** * Creates a character key press */ createCharacterKey(character, sequenceIndex, isCapsLock) { const key = character.toLowerCase(); const type = character.match(/[a-zA-Z]/) ? "letter" : character.match(/[0-9]/) ? "number" : "symbol"; const baseDuration = this.layout.keyDurations[type]; const isComplexSymbol = type === "symbol" && this.isComplexSymbol(character); const contextualDuration = calculateContextualTiming(baseDuration, this.timingProfile, { isComplexSymbol }); return { key, character, type, keyboardView: this.state.currentView, isCapsLock, duration: contextualDuration, sequenceIndex, sequenceLength: 0 }; } /** * Updates recent characters for caps lock detection */ updateRecentCharacters(character) { this.state.recentCharacters.push(character); if (this.state.recentCharacters.length > 10) { this.state.recentCharacters.shift(); } } /** * Analyzes whether character is part of a caps lock sequence * Uses the same logic as the original TypingEngine */ analyzeCapsLockSequence(character, charIndex, fullText) { if (!character.match(/[A-Z]/)) { return { isCapsLockSequence: false, isFirst: false, isLast: false }; } let sequenceStart = charIndex; let sequenceEnd = charIndex; while (sequenceStart > 0) { const prevChar = fullText[sequenceStart - 1]; if (prevChar.match(/[A-Z]/) || prevChar === " ") { sequenceStart--; } else { break; } } while (sequenceEnd < fullText.length - 1) { const nextChar = fullText[sequenceEnd + 1]; if (nextChar.match(/[A-Z]/) || nextChar === " ") { sequenceEnd++; } else { break; } } let capitalCount = 0; for (let i = sequenceStart; i <= sequenceEnd; i++) { if (fullText[i].match(/[A-Z]/)) { capitalCount++; } } const isCapsLockSequence = capitalCount >= this.config.capsLockThreshold; let firstCapitalIndex = sequenceStart; while (firstCapitalIndex <= sequenceEnd && !fullText[firstCapitalIndex].match(/[A-Z]/)) { firstCapitalIndex++; } let lastCapitalIndex = sequenceEnd; while (lastCapitalIndex >= sequenceStart && !fullText[lastCapitalIndex].match(/[A-Z]/)) { lastCapitalIndex--; } return { isCapsLockSequence, isFirst: isCapsLockSequence && charIndex === firstCapitalIndex, isLast: isCapsLockSequence && charIndex === lastCapitalIndex }; } /** * Check if a symbol is considered complex (requires more precise movement) */ isComplexSymbol(character) { const complexSymbols = /* @__PURE__ */ new Set(["@", "#", "$", "%", "^", "&", "*", "+", "=", "{", "}", "\\", "|", "`", "~", "<", ">"]); return complexSymbols.has(character); } /** * Debug logging */ debug(message, data) { if (this.config.debug) { console.log(`[KeyboardAnalyzer] ${message}`, data || ""); } } /** * Reset keyboard state (useful for testing) */ resetState() { this.state = { currentView: "letters", capsLockActive: false, shiftActive: false, recentCharacters: [], mode: this.config.keyboardMode }; } /** * Analyzes a backspace key press and returns the key sequence */ analyzeBackspace() { this.debug("Analyzing backspace key"); const backspaceKey = { key: this.config.keyboardMode === "mobile" ? this.layout.modifiers.backspace : "backspace", character: "\b", // Backspace character type: "backspace", keyboardView: this.state.currentView, isCapsLock: false, duration: this.layout.keyDurations.backspace || this.layout.keyDurations.modifier || 120, sequenceIndex: 0, sequenceLength: 1 }; const sequence = { character: "\b", keys: [backspaceKey], totalDuration: backspaceKey.duration, usesCapsLock: false }; this.debug("Generated backspace sequence:", sequence); return sequence; } /** * Get current keyboard state (useful for debugging) */ getState() { return { ...this.state }; } } class TypingEngine { constructor(text, config = {}) { __publicField(this, "config"); __publicField(this, "text"); __publicField(this, "currentIndex", 0); __publicField(this, "displayText", ""); __publicField(this, "state", "idle"); __publicField(this, "timeoutId", null); __publicField(this, "keyTimeouts", /* @__PURE__ */ new Set()); // Track all keyboard timing timeouts __publicField(this, "stats"); __publicField(this, "events", []); __publicField(this, "mistakes", []); __publicField(this, "correctionQueue", []); __publicField(this, "isCorrectingMistake", false); __publicField(this, "charactersTyped", 0); __publicField(this, "fatigueLevel", 0); __publicField(this, "pauseStartTime", 0); __publicField(this, "totalPausedTime", 0); // Keyboard simulation - always enabled now __publicField(this, "keyboardAnalyzer"); // Event callbacks __publicField(this, "onStateChange"); __publicField(this, "onCharacter"); __publicField(this, "onMistake"); __publicField(this, "onBackspace"); __publicField(this, "onComplete"); __publicField(this, "onProgress"); __publicField(this, "onKey"); this.text = text ?? ""; this.config = { ...DEFAULT_CONFIG, ...config }; this.stats = this.initializeStats(); if (this.config.onKey) { this.onKey = this.config.onKey; } this.keyboardAnalyzer = new KeyboardAnalyzer({ keyboardMode: this.config.keyboardMode || "mobile", capsLockThreshold: TIMING_CONSTANTS.CAPS_SEQUENCE_THRESHOLD, useNaturalTiming: true, // Always use natural keyboard timing debug: this.config.debug }); } debug(...args) { if (this.config.debug) { console.log(...args); } } safeCallback(callback, ...args) { if (callback) { try { callback(...args); } catch (error) { if (this.config.debug) { console.warn("Callback error:", error); } } } } initializeStats() { return { totalCharacters: this.text.length, charactersTyped: 0, mistakesMade: 0, mistakesCorrected: 0, startTime: 0, currentWPM: 0, averageCharDelay: 0, totalDuration: 0 }; } start() { if (this.state === "typing" || this.state === "correcting") return; this.state = "typing"; const now = Date.now(); if (this.stats.startTime === 0) { this.stats.startTime = now; } this.safeCallback(this.onStateChange, "typing"); this.scheduleNextCharacter(); } stop() { if (this.timeoutId) { clearTimeout(this.timeoutId); this.timeoutId = null; } this.keyTimeouts.forEach((timeoutId) => clearTimeout(timeoutId)); this.keyTimeouts.clear(); for (const mistake of this.mistakes) { if (!mistake.corrected) { mistake.corrected = true; } } this.correctionQueue = []; this.isCorrectingMistake = false; this.state = "idle"; this.safeCallback(this.onStateChange, "idle"); } pause() { if (this.state === "typing" || this.state === "correcting") { if (this.timeoutId) { clearTimeout(this.timeoutId); this.timeoutId = null; } this.pauseStartTime = Date.now(); this.state = "paused"; this.safeCallback(this.onStateChange, "paused"); } } resume() { if (this.state === "paused") { if (this.pauseStartTime > 0) { this.totalPausedTime += Date.now() - this.pauseStartTime; this.pauseStartTime = 0; } this.state = "typing"; this.safeCallback(this.onStateChange, "typing"); this.scheduleNextCharacter(); } } skip() { this.currentIndex = this.text.length; this.displayText = this.text; this.state = "completed"; this.stats.endTime = Date.now(); this.safeCallback(this.onStateChange, "completed"); this.safeCallback(this.onComplete); } reset() { this.stop(); this.currentIndex = 0; this.displayText = ""; this.charactersTyped = 0; this.fatigueLevel = 0; this.mistakes = []; this.correctionQueue = []; this.isCorrectingMistake = false; this.events = []; this.pauseStartTime = 0; this.totalPausedTime = 0; this.stats = this.initializeStats(); this.state = "idle"; this.safeCallback(this.onStateChange, "idle"); } scheduleNextCharacter() { if (this.text.length === 0) { this.completeTyping(); return; } if (this.currentIndex >= this.text.length) { const hasUncorrectedMistakes = this.mistakes.some((m) => !m.corrected); const hasPendingCorrections = this.correctionQueue.length > 0; if (hasUncorrectedMistakes || hasPendingCorrections) { this.debug(`📝 Reached end but have ${this.mistakes.filter((m) => !m.corrected).length} uncorrected mistakes and ${this.correctionQueue.length} queued corrections`); this.processNextCorrection(); return; } if (this.keyTimeouts.size > 0) { this.debug(`⏳ Reached end but waiting for ${this.keyTimeouts.size} key timeouts to complete`); this.timeoutId = window.setTimeout(() => { this.scheduleNextCharacter(); }, 50); return; } this.completeTyping(); return; } const char = this.text[this.currentIndex]; const delay = this.calculateCharacterDelayWithKeyboard(char); if (this.currentIndex > 0 && this.shouldHaveConcentrationLapse()) { this.state = "thinking"; this.safeCallback(this.onStateChange, "thinking"); this.timeoutId = window.setTimeout(() => { this.state = "typing"; this.safeCallback(this.onStateChange, "typing"); this.scheduleNextCharacter(); }, TIMING_CONSTANTS.CONCENTRATION_PAUSE); return; } const shouldMakeMistake = this.shouldMakeMistake(char); if (delay < 30) { requestAnimationFrame(() => { if (shouldMakeMistake) { this.makeMistake(char); } else { this.typeCharacter(char); } }); } else { this.timeoutId = window.setTimeout(() => { if (shouldMakeMistake) { this.makeMistake(char); } else { this.typeCharacter(char); } }, delay); } } /** * Calculate character delay using natural keyboard timing * This revolutionary approach replaces artificial delays with realistic key sequences */ calculateCharacterDelayWithKeyboard(char) { const keySequence = this.keyboardAnalyzer.analyzeCharacter(char, this.currentIndex, this.text); const speedMultiplier = this.config.speed / 80; const scaledKeys = keySequence.keys.map((keyInfo) => ({ ...keyInfo, duration: Math.round(keyInfo.duration * speedMultiplier) })); const scaledTotalDuration = scaledKeys.reduce((sum, key) => sum + key.duration, 0); this.debug(`🎹 Natural timing for "${char}": ${scaledKeys.length} keys, ${scaledTotalDuration}ms total (speed: ${this.config.speed}ms, multiplier: ${speedMultiplier.toFixed(2)})`); let cumulativeDelay = 0; scaledKeys.forEach((keyInfo) => { const keyTimeoutId = window.setTimeout(() => { this.safeCallback(this.onKey, keyInfo); this.debug(`🔑 Key press: "${keyInfo.key}" (${keyInfo.type}) - ${keyInfo.duration}ms`); this.keyTimeouts.delete(keyTimeoutId); }, cumulativeDelay); this.keyTimeouts.add(keyTimeoutId); cumulativeDelay += keyInfo.duration; }); let totalDelay = scaledTotalDuration; const variation = (Math.random() - 0.5) * 2 * this.config.speedVariation; totalDelay += variation; if (this.config.fatigueEffect) { totalDelay += this.fatigueLevel; this.fatigueLevel += TIMING_CONSTANTS.FATIGUE_INCREMENT; } if (Math.random() < BEHAVIOR_RATES.BURST_TYPING) { totalDelay *= TIMING_CONSTANTS.BURST_SPEED_MULTIPLIER; } if (SENTENCE_ENDINGS.has(char)) { totalDelay += this.config.sentencePause; } else if (CLAUSE_SEPARATORS.has(char)) { totalDelay += TIMING_CONSTANTS.COMMA_PAUSE; } else if (LINE_BREAK_CHARS.has(char)) { totalDelay += TIMING_CONSTANTS.LINE_BREAK; } else if (char === " ") { totalDelay += this.config.wordPause; const nextWord = this.getNextWord(); if (nextWord && this.isComplexWord(nextWord)) { totalDelay += this.config.thinkingPause; } } return Math.max(this.config.minCharDelay, totalDelay); } shouldMakeMistake(char) { if (char === " " || LINE_BREAK_CHARS.has(char) || this.currentIndex === 0) return false; let mistakeChance = this.config.mistakeFrequency; if (NUMBER_CHARS.has(char)) { mistakeChance *= 1.5; } if (SHIFT_CHARS.has(char)) { mistakeChance *= 1.2; } if (SYMBOL_COMPLEXITY[char] && SYMBOL_COMPLEXITY[char] >= 3) { mistakeChance *= 1.3; } if (SPECIAL_CHARS.has(char) && (!SYMBOL_COMPLEXITY[char] || SYMBOL_COMPLEXITY[char] === 1)) { mistakeChance *= 0.5; } if (this.shouldMakeLookAheadMistake()) { mistakeChance *= 2; } return Math.random() < mistakeChance; } makeMistake(originalChar) { const mistakeType = this.selectMistakeType(originalChar); const mistakeChar = this.generateMistakeChar(originalChar, mistakeType); if (!mistakeChar) { this.typeCharacter(originalChar); return; } const mistake = { type: mistakeType, originalChar, mistakeChar, position: this.currentIndex, corrected: false, realizationTime: this.config.realizationDelay + Math.random() * 150 }; this.mistakes.push(mistake); this.stats.mistakesMade++; this.debug(`🔴 MISTAKE: "${originalChar}" → "${mistakeChar}" (${mistakeType}) at pos ${this.currentIndex}`); this.displayText += mistakeChar; this.currentIndex++; this.charactersTyped++; this.stats.charactersTyped = this.charactersTyped; this.recordEvent({ type: "mistake", position: mistake.position, timestamp: Date.now(), char: mistakeChar, mistake }); this.safeCallback(this.onCharacter, mistakeChar, this.currentIndex - mistakeChar.length); this.safeCallback(this.onMistake, mistake); this.updateProgress(); const realizationTime = Math.max(mistake.realizationTime, 200); this.debug(`📝 Adding "${mistakeChar}" to correction queue (will correct in ${realizationTime}ms)`); this.timeoutId = window.setTimeout(() => { if (!mistake.corrected) { this.correctionQueue.push(mistake); this.processNextCorrection(); } }, realizationTime); } processNextCorrection() { if (this.isCorrectingMistake || this.correctionQueue.length === 0) { return; } const mistake = this.correctionQueue.pop(); if (mistake.corrected) { this.processNextCorrection(); return; } this.debug(`🔧 Processing correction for "${mistake.mistakeChar}" → "${mistake.originalChar}"`); this.correctMistake(mistake); } correctMistake(mistake) { this.isCorrectingMistake = true; this.state = "correcting"; this.safeCallback(this.onStateChange, "correcting"); this.debug(`🔧 Correcting mistake: "${mistake.mistakeChar}" → should be "${mistake.originalChar}"`); this.debug(`📍 Current text: "${this.displayText}", current position: ${this.currentIndex}, mistake position: ${mistake.position}`); const targetPosition = mistake.position; const currentTextLength = this.displayText.length; const charsToDelete = currentTextLength - targetPosition; this.debug(`🔄 Need to delete ${charsToDelete} chars to get back to position ${targetPosition}`); if (charsToDelete <= 0) { this.debug(`⚠️ Already at correct position, marking as corrected`); this.finishCorrection(mistake); return; } this.performBackspaceSequence(mistake, charsToDelete, targetPosition); } performBackspaceSequence(mistake, charsToDelete, targetPosition) { let deletedCount = 0; const backspaceDelay = this.config.backspaceSpeed; const performBackspace = () => { if (deletedCount < charsToDelete && this.displayText.length > targetPosition) { this.displayText = this.displayText.slice(