import './style.css' import { generateMosaic, selectKeywords, type KeywordCandidate, } from './mosaic' const SAMPLE_TEXT = '我一直以为记忆会忠实地保存一切,后来才发现,每一次回想都在重新编造过去。' const app = document.querySelector('#app') if (!app) { throw new Error('找不到应用挂载节点 #app') } app.innerHTML = `

文字马赛克

让文字损坏,只留下几处可辨认的痕迹。

01

原始文本

0 字
02

损坏结果

CONTROL SIGNAL

侵蚀参数

03

保留词

点按词语切换显影状态,原始顺序不会改变。

LOCAL PROCESSING

文本只在当前浏览器中处理,不会上传。

` const sourceInput = getElement('source-text') const inputCount = getElement('input-count') const outputRatio = getElement('output-ratio') const mosaicOutput = getElement('mosaic-output') const keywordList = getElement('keyword-list') const toast = getElement('toast') const retentionControl = getElement('retention-control') const volumeControl = getElement('volume-control') const disorderControl = getElement('disorder-control') const retentionValue = getElement('retention-value') const volumeValue = getElement('volume-value') const disorderValue = getElement('disorder-value') const rerollButton = getElement('reroll-button') const copyButton = getElement('copy-button') const reselectButton = getElement('reselect-button') let candidates: KeywordCandidate[] = [] let selectionSeed = 5 let noiseSeed = Date.now() let toastTimer = 0 function getElement(id: string): T { const element = document.getElementById(id) if (!element) { throw new Error(`找不到页面元素 #${id}`) } return element as T } function showToast(message: string): void { window.clearTimeout(toastTimer) toast.textContent = message toast.classList.add('toast--visible') toastTimer = window.setTimeout(() => { toast.classList.remove('toast--visible') }, 1800) } function getRetention(): number { return Number(retentionControl.value) } function renderKeywords(): void { keywordList.replaceChildren() if (candidates.length === 0) { const empty = document.createElement('p') empty.className = 'keyword-list__empty' empty.textContent = sourceInput.value.trim() ? '没有识别到可保留的词语,可以尝试输入更长的中文文本。' : '输入文字后,这里会出现可保留的词语。' keywordList.append(empty) return } for (const candidate of candidates) { const button = document.createElement('button') button.type = 'button' button.className = 'keyword-token' button.textContent = candidate.text button.setAttribute('aria-pressed', String(candidate.selected)) if (candidate.selected) { button.classList.add('keyword-token--selected') } button.addEventListener('click', () => { candidate.selected = !candidate.selected renderKeywords() renderOutput() }) keywordList.append(button) } } function refreshCandidates(): void { candidates = selectKeywords(sourceInput.value, getRetention(), selectionSeed) renderKeywords() renderOutput() } function renderOutput(): void { const result = generateMosaic(sourceInput.value, candidates, { volume: Number(volumeControl.value), disorder: Number(disorderControl.value), seed: noiseSeed, }) mosaicOutput.textContent = result.output || '等待文字进入损坏区' mosaicOutput.classList.toggle('mosaic-output--empty', result.output === '') inputCount.textContent = `${result.inputLength} 字` outputRatio.textContent = result.inputLength === 0 ? '—' : `${result.outputLength} 字 / ${Math.round((result.outputLength / result.inputLength) * 100)}%` } function syncControls(): void { retentionValue.value = `${retentionControl.value}%` volumeValue.value = `${volumeControl.value}%` disorderValue.value = `${disorderControl.value}%` } sourceInput.addEventListener('input', () => { selectionSeed += 1 noiseSeed += 1 refreshCandidates() }) retentionControl.addEventListener('input', () => { syncControls() refreshCandidates() }) volumeControl.addEventListener('input', () => { syncControls() renderOutput() }) disorderControl.addEventListener('input', () => { syncControls() renderOutput() }) rerollButton.addEventListener('click', () => { noiseSeed += 1 renderOutput() }) reselectButton.addEventListener('click', () => { selectionSeed += 1 refreshCandidates() }) copyButton.addEventListener('click', async () => { const text = mosaicOutput.textContent ?? '' if (!sourceInput.value || !text) { showToast('还没有可以复制的结果') return } try { await navigator.clipboard.writeText(text) showToast('已复制纯文本') } catch (error) { console.error('复制文字马赛克失败', error) showToast('复制失败,请手动选择文本') } }) sourceInput.value = SAMPLE_TEXT syncControls() refreshCandidates()