From ca0f20c5cacd117ac0964900e051f93e0d67074d Mon Sep 17 00:00:00 2001 From: Andre Padez Date: Tue, 4 Nov 2025 18:13:18 +0000 Subject: [PATCH] Fix paragraph line order and remove red text effect on misses - Fixed paragraph display: first typed line now correctly appears at bottom, matching the Star Wars text crawl effect - Removed red text flash when typing incorrectly - only red laser effect remains for visual feedback - Adjusted perspective factors to properly scale lines from bottom (largest) to top (smallest) - Clean up: removed all 'wrong' state handling from both rendering and game logic --- src/useBoard/useBoard.ts | 185 +++++++++++++++++++---------- src/useGameStatus/useGameStatus.ts | 11 +- src/wordLists.ts | 10 +- 3 files changed, 131 insertions(+), 75 deletions(-) diff --git a/src/useBoard/useBoard.ts b/src/useBoard/useBoard.ts index 09a0ea2..460731d 100644 --- a/src/useBoard/useBoard.ts +++ b/src/useBoard/useBoard.ts @@ -5,24 +5,51 @@ const CANVAS_WIDTH = 800; const CANVAS_HEIGHT = 600; const SPACESHIP_Y = CANVAS_HEIGHT - 40; -// Helper function to break text into two lines at a good breaking point -const breakTextIntoLines = (text: string): [string, string] => { - const midpoint = Math.ceil(text.length / 2); - - // Try to find a space near the midpoint to break naturally - let breakPoint = midpoint; - for (let i = midpoint; i >= midpoint - 10 && i >= 0; i--) { - if (text[i] === ' ') { - breakPoint = i; - break; +// Helper function to break text into multiple lines at good breaking points +const breakTextIntoLines = (text: string, numLines: number = 2): string[] => { + if (numLines === 2) { + const midpoint = Math.ceil(text.length / 2); + + // Try to find a space near the midpoint to break naturally + let breakPoint = midpoint; + for (let i = midpoint; i >= midpoint - 10 && i >= 0; i--) { + if (text[i] === ' ') { + breakPoint = i; + break; + } } + + // Keep the space at the end of line1 for typing + const line1 = text.substring(0, breakPoint + 1); // Include the space + const line2 = text.substring(breakPoint + 1).trim(); // Start after the space + + return [line1, line2]; } - // Keep the space at the end of line1 for typing - const line1 = text.substring(0, breakPoint + 1); // Include the space - const line2 = text.substring(breakPoint + 1).trim(); // Start after the space + // For 3+ lines, divide text evenly + const lines: string[] = []; + const charsPerLine = Math.ceil(text.length / numLines); + let startIdx = 0; - return [line1, line2]; + for (let i = 0; i < numLines - 1; i++) { + let endIdx = startIdx + charsPerLine; + + // Find nearest space to break at + for (let j = endIdx; j >= endIdx - 10 && j >= startIdx; j--) { + if (text[j] === ' ') { + endIdx = j; + break; + } + } + + lines.push(text.substring(startIdx, endIdx + 1)); // Include the space + startIdx = endIdx + 1; + } + + // Last line gets remaining text + lines.push(text.substring(startIdx).trim()); + + return lines; }; export function useBoard() { @@ -249,49 +276,7 @@ export function useBoard() { // For sentences/paragraphs, always center horizontally const letterX = isSentenceMode ? CANVAS_WIDTH / 2 : letter.x; - if (letter.state === 'wrong') { - // Red glow flash - ctx.shadowBlur = 20; - ctx.shadowColor = "#ff0000"; - ctx.fillStyle = "#ff4444"; - - const displayText = letter.fullText || letter.letter; - - if (isSentenceMode && (gameMode === 'sentences' || gameMode === 'paragraphs')) { - // For sentences: break into two lines with Star Wars effect - const [line1, line2] = breakTextIntoLines(displayText); - - ctx.save(); - - // Star Wars perspective effect - const perspectiveFactor = 0.8; - const vanishingPointX = CANVAS_WIDTH / 2; - - // Inverted: line2 on top, line1 on bottom - const line2Y = letter.y - fontSize * 1.5; - const line1Y = letter.y + fontSize; - - // Draw line2 on top with perspective - const line2FontSize = fontSize * perspectiveFactor; - ctx.font = `bold ${line2FontSize}px monospace`; - const line2CenterX = letterX + (vanishingPointX - letterX) * 0.2; - const line2Width = ctx.measureText(line2).width; - ctx.fillText(line2, line2CenterX - line2Width / 2, line2Y); - - // Draw line1 on bottom (full size) - ctx.font = `bold ${fontSize}px monospace`; - const line1Width = ctx.measureText(line1).width; - ctx.fillText(line1, letterX - line1Width / 2, line1Y); - - ctx.restore(); - } else { - // Single line for letters/words - const textWidth = ctx.measureText(displayText).width; - ctx.fillText(displayText, letterX - textWidth / 2, letter.y); - } - - ctx.shadowBlur = 0; - } else if (letter.state === 'exploding') { + if (letter.state === 'exploding') { // Explosion effect const elapsed = performance.now() - (letter.stateStartTime || 0); const progress = Math.min(elapsed / 300, 1); @@ -320,7 +305,29 @@ export function useBoard() { ctx.fillStyle = "#ffffff"; const displayText = letter.fullText || letter.letter; - if (isSentenceMode && (gameMode === 'sentences' || gameMode === 'paragraphs')) { + if (gameMode === 'paragraphs') { + // For paragraphs: 4 lines (first typed at bottom, last typed at top) + const lines = breakTextIntoLines(displayText, 4); + const perspectiveFactors = [0.6, 0.7, 0.85, 1.0]; // Reversed: smallest at top, largest at bottom + const lineSpacing = fontSize * 1.2; + + ctx.save(); + + lines.forEach((line, lineIndex) => { + // Calculate visual position: line 0 at bottom, line 3 at top + const visualIndex = lines.length - 1 - lineIndex; + const lineY = letter.y + (lineSpacing * (visualIndex - 1.5)); + const lineFontSize = fontSize * perspectiveFactors[visualIndex]; + const convergence = 0.3 * (1 - perspectiveFactors[visualIndex]); + const lineCenterX = letterX + ((CANVAS_WIDTH / 2) - letterX) * convergence; + + ctx.font = `bold ${lineFontSize}px monospace`; + const lineWidth = ctx.measureText(line).width; + ctx.fillText(line, lineCenterX - lineWidth / 2, lineY); + }); + + ctx.restore(); + } else if (isSentenceMode && gameMode === 'sentences') { // For sentences: break into two lines during explosion with perspective const [line1, line2] = breakTextIntoLines(displayText); @@ -349,9 +356,65 @@ export function useBoard() { const fullText = letter.fullText || letter.letter; const charIndex = letter.charIndex ?? 0; - if (isSentenceMode && (gameMode === 'sentences' || gameMode === 'paragraphs')) { - // For sentences/paragraphs: break into two lines with Star Wars effect - const [line1, line2] = breakTextIntoLines(fullText); + if (gameMode === 'paragraphs') { + // Paragraphs: break into 4 lines with enhanced Star Wars effect + const lines = breakTextIntoLines(fullText, 4); + + // Calculate character positions for line breaks + let totalChars = 0; + const lineStartIndices = [0]; + for (let i = 0; i < lines.length - 1; i++) { + totalChars += lines[i].length; + lineStartIndices.push(totalChars); + } + + // Enhanced Star Wars perspective for 4 lines + // First typed line (index 0) at bottom, last typed line (index 3) at top + const perspectiveFactors = [0.6, 0.7, 0.85, 1.0]; // Reversed: smallest at top, largest at bottom + const lineSpacing = fontSize * 1.2; + + ctx.save(); + + // Render each line with perspective + lines.forEach((line, lineIndex) => { + // Calculate visual position: line 0 at bottom, line 3 at top + const visualIndex = lines.length - 1 - lineIndex; + const lineY = letter.y + (lineSpacing * (visualIndex - 1.5)); + const lineFontSize = fontSize * perspectiveFactors[visualIndex]; + const lineOpacity = 1.0 - (visualIndex * 0.1); // Decreasing opacity for lines at top + + // Convergence toward vanishing point (more for lines at top) + const convergence = 0.3 * (1 - perspectiveFactors[visualIndex]); + const lineCenterX = letterX + ((CANVAS_WIDTH / 2) - letterX) * convergence; + + ctx.font = `bold ${lineFontSize}px monospace`; + const lineWidth = ctx.measureText(line).width; + let lineX = lineCenterX - lineWidth / 2; + + // Render characters with color coding + for (let i = 0; i < line.length; i++) { + const globalCharIdx = lineStartIndices[lineIndex] + i; + const char = line[i]; + + if (globalCharIdx < charIndex) { + ctx.fillStyle = `rgba(0, 255, 0, ${lineOpacity})`; // Green - completed + } else if (globalCharIdx === charIndex) { + ctx.fillStyle = `rgba(255, 255, 0, ${lineOpacity})`; // Yellow - current + // Store position for ship tracking + letter.currentCharX = lineX + ctx.measureText(char).width / 2; + } else { + ctx.fillStyle = `rgba(255, 255, 255, ${lineOpacity})`; // White - remaining + } + + ctx.fillText(char, lineX, lineY); + lineX += ctx.measureText(char).width; + } + }); + + ctx.restore(); + } else if (isSentenceMode && gameMode === 'sentences') { + // For sentences: keep the existing 2-line system + const [line1, line2] = breakTextIntoLines(fullText, 2); const line1Length = line1.length; // Check if we're in a line transition animation diff --git a/src/useGameStatus/useGameStatus.ts b/src/useGameStatus/useGameStatus.ts index de84434..85dd324 100755 --- a/src/useGameStatus/useGameStatus.ts +++ b/src/useGameStatus/useGameStatus.ts @@ -6,7 +6,7 @@ const LETTER_SPEEDS: Record = { letters: 100, words: 50, sentences: 25, - paragraphs: 50, + paragraphs: 15, // Even slower for longer text }; const SPAWN_INTERVAL = 1000; // ms between letter spawns const MAX_ACTIVE_LETTERS = 1; // Only one letter on screen at a time @@ -202,8 +202,7 @@ export function useGameStatus(onGameLoopTick?: (deltaTime: number, letters: Fall currentLetter.letter = fullText[charIndex]; } } else { - currentLetter.state = 'wrong'; - currentLetter.stateStartTime = performance.now(); + // Don't change the letter's visual state - only show red laser setScore(prev => Math.max(0, prev - 1)); setMissed(prev => prev + 1); @@ -226,12 +225,6 @@ export function useGameStatus(onGameLoopTick?: (deltaTime: number, letters: Fall }; lasersRef.current.push(newLaser); - - setTimeout(() => { - if (currentLetter.state === 'wrong') { - currentLetter.state = 'normal'; - } - }, 200); } }; diff --git a/src/wordLists.ts b/src/wordLists.ts index 9184e1e..33ff9fd 100644 --- a/src/wordLists.ts +++ b/src/wordLists.ts @@ -29,11 +29,11 @@ export const wordLists = { ] as const, paragraphs: [ - 'the quick brown fox jumps over the lazy dog', - 'practice typing to improve your speed and accuracy', - 'focus on each word and type with purpose today', - 'consistency in practice leads to better results', - 'challenge yourself with longer text passages', + 'The quick brown fox jumps over the lazy dog, demonstrating all letters of the alphabet in one swift sentence.', + 'Practice typing every day to improve your speed and accuracy, because consistent effort leads to mastery over time.', + 'In a galaxy far, far away, a young hero learns to type with incredible speed, defending the universe one keystroke at a time.', + 'Technology advances rapidly, but the skill of typing remains essential for communication, coding, and creative expression in our digital world.', + 'Challenge yourself with increasingly difficult passages, pushing your limits and watching your words per minute soar to new heights!', ] as const, };