import React from 'react'; import { Mic, Volume2, MessageSquare } from 'lucide-react'; export default function WordsTab({ wordType, setWordType, voiceWords, chatWords }) { const currentWordsList = wordType === 'voice' ? (Array.isArray(voiceWords) ? voiceWords : []) : (Array.isArray(chatWords) ? chatWords : []); if (currentWordsList.length === 0) { return (

Частотный словарь

{wordType === 'voice' ? 'Слова, которые стример произнес в микрофон (распознанные через Whisper на локальном ПК).' : 'Слова, которые зрители написали в текстовый чат Twitch.'}

Нет собранных слов для выбранного стрима

); } const counts = currentWordsList.map(x => x.word_count); const maxCount = Math.max(...counts, 1); const minCount = Math.min(...counts, 1); // Helper for Russian pluralization const getRussianPlural = (count) => { const lastDigit = count % 10; const lastTwoDigits = count % 100; if (lastTwoDigits >= 11 && lastTwoDigits <= 19) { return 'раз'; } if (lastDigit === 1) { return 'раз'; } if (lastDigit >= 2 && lastDigit <= 4) { return 'раза'; } return 'раз'; }; // Color assignment based on word frequency: purple gradient const getWordColor = (count) => { const scale = maxCount === minCount ? 1 : (count - minCount) / (maxCount - minCount); // Scale saturation from 35% (desaturated/white-lavender) to 100% (saturated purple) const sat = Math.round(35 + scale * 65); // Scale lightness from 92% (light/white-ish) to 60% (vivid deep purple) const light = Math.round(92 - scale * 32); return `hsl(265, ${sat}%, ${light}%)`; }; // Layout calculations with collision avoidance on stretched elliptical spiral const placedBoxes = []; const laidOutWords = []; const isMobile = typeof window !== 'undefined' && window.innerWidth < 768; const stretchX = isMobile ? 1.4 : 2.8; const stretchY = 1.0; const paddingX = isMobile ? 6 : 12; const paddingY = isMobile ? 4 : 8; const fontScale = isMobile ? 0.45 : 1.0; const remToPx = 16; const centerGroupThreshold = wordType === 'chat' ? 250 : 500; // Identify center group: words with count within threshold of the max count, // but ONLY if the top count is >= threshold. Also cap center group size to at most 3 words. const centerGroupWords = currentWordsList.filter((w, idx) => idx === 0 || (maxCount >= centerGroupThreshold && (maxCount - w.word_count) < centerGroupThreshold && idx < 3) ); const otherWordsList = currentWordsList.filter((w, idx) => !(idx === 0 || (maxCount >= centerGroupThreshold && (maxCount - w.word_count) < centerGroupThreshold && idx < 3)) ); const maxOtherCount = otherWordsList.length > 0 ? Math.max(...otherWordsList.map(x => x.word_count)) : 1; const minOtherCount = otherWordsList.length > 0 ? Math.min(...otherWordsList.map(x => x.word_count)) : 1; const minCenterCount = Math.min(...centerGroupWords.map(x => x.word_count)); // Function to estimate word width factor based on wide/narrow letters const getWordWidthFactor = (word) => { let factor = 0; const wideChars = /[мжшщыюяwm]/i; const narrowChars = /[ilj1!|т]/i; for (const char of word) { if (wideChars.test(char)) { factor += 0.75; } else if (narrowChars.test(char)) { factor += 0.35; } else { factor += 0.55; } } return factor; }; for (let i = 0; i < currentWordsList.length; i++) { const w = currentWordsList[i]; const isCenterGroup = i === 0 || (maxCount >= centerGroupThreshold && (maxCount - w.word_count) < centerGroupThreshold && i < 3); let fontSize; let fontWeight; let scaleValue = 0; // Scale down the entire galaxy if the absolute max frequency is low (e.g. less than 200) // If maxCount is 20, galaxyScale is ~0.65. If 200+, it's 1.0. const galaxyScale = Math.min(Math.max(maxCount / 200, 0), 1.0) * 0.35 + 0.65; if (isCenterGroup) { // Scale font size within center group: ranges from 4.5rem to 6.2rem const centerScale = maxCount === minCenterCount ? 1 : (w.word_count - minCenterCount) / (maxCount - minCenterCount); fontSize = 4.5 + centerScale * 1.7; fontWeight = '900'; scaleValue = centerScale; } else { // Scale font size within other words: ranges from 1.1rem to 3.2rem const scale = maxOtherCount === minOtherCount ? 1 : (w.word_count - minOtherCount) / (maxOtherCount - minOtherCount); scaleValue = Math.pow(scale, 1.4); fontSize = 1.1 + scaleValue * 2.1; fontWeight = fontSize > 2.0 ? '700' : (fontSize > 1.4 ? '600' : '500'); } fontSize = fontSize * fontScale * galaxyScale; // Precise width calculation based on custom width factors const wordWidth = fontSize * getWordWidthFactor(w.word) * remToPx; const wordHeight = fontSize * 1.1 * remToPx; let x = 0; let y = 0; // Generate a stable hash for the word to randomize angle and jitter let hash = 0; for (let j = 0; j < w.word.length; j++) { hash = w.word.charCodeAt(j) + ((hash << 5) - hash); } hash = Math.abs(hash); // Search for position starting from r = 0. // Since center group words are processed first, they cluster tightly around (0,0). let found = false; const rStep = 1.1; for (let attempt = 0; attempt < 1200; attempt++) { const startAngle = (hash % 100) * 0.06283; // 0 to 2*PI const angle = startAngle + attempt * 0.15; const r = (i === 0 && attempt === 0) ? 0 : (45 + rStep * attempt); x = r * Math.cos(angle) * stretchX; y = r * Math.sin(angle) * stretchY; // Add coordinate noise/jitter (except for the absolute top word at the center) if (r > 0) { x += Math.sin(hash * 0.5 + attempt) * 6; y += Math.cos(hash * 0.8 + attempt) * 4; } let collision = false; for (const box of placedBoxes) { const halfW1 = wordWidth / 2; const halfH1 = wordHeight / 2; const halfW2 = box.w / 2; const halfH2 = box.h / 2; if (Math.abs(x - box.x) < (halfW1 + halfW2 + paddingX) && Math.abs(y - box.y) < (halfH1 + halfH2 + paddingY)) { collision = true; break; } } if (!collision) { found = true; break; } } placedBoxes.push({ x: x, y: y, w: wordWidth, h: wordHeight }); const color = getWordColor(w.word_count); // Floating parameters const duration = 4.5 + (i % 3) + (i % 4) * 0.4; // 4.5s to 8.5s const delay = -((i * 1.3) % 7); // negative delay to start asynchronously const amount = 3 + (i % 4); // 3px to 6px float amount laidOutWords.push({ word: w.word, count: w.word_count, x: Math.round(x), y: Math.round(y), fontSize: `${fontSize}rem`, fontWeight: fontWeight, color: color, scale: scaleValue, isCenterGroup: isCenterGroup, duration: `${duration}s`, delay: `${delay}s`, amount: `${amount}px` }); } return (

Частотный словарь

{wordType === 'voice' ? 'Слова, которые стример произнес в микрофон (распознанные через Whisper на локальном ПК).' : 'Слова, которые зрители написали в текстовый чат Twitch.'}

{laidOutWords.map((w, idx) => (
{w.word}
{w.count} {getRussianPlural(w.count)}
))}
); }