TEXT MIXER - INTERACTIVE APP SOURCE CODE
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Text Mixer — character n‑gram · Markov chain · cento</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/tabler-icons.min.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Special+Elite&family=DM+Mono:ital,wght@0,300;0,400;1,300&display=swap" rel="stylesheet">
<style>
:root {
color-scheme: light dark;
--color-text-primary: #e8e6e3;
--color-text-secondary: #b0ada8;
--color-text-tertiary: #7a7672;
--color-background-primary: #121212;
--color-background-secondary: #1e1e1e;
--color-border-primary: #e8e6e3;
--color-border-secondary: #3a3a3a;
--color-border-tertiary: #2a2a2a;
--color-border-danger: #e05555;
--border-radius-md: 8px;
}
html:not(.dark) {
--color-text-primary: #121212;
--color-text-secondary: #4a4a4a;
--color-text-tertiary: #7a7672;
--color-background-primary: #f5f5f5;
--color-background-secondary: #ffffff;
--color-border-primary: #121212;
--color-border-secondary: #cccccc;
--color-border-tertiary: #e0e0e0;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html,
body {
margin: 0;
padding: 0;
background: transparent;
min-height: 100vh;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.mixer-wrap {
font-family: 'DM Mono', monospace;
width: 100%;
}
.mixer-title {
font-family: 'Special Elite', cursive;
font-size: 28px;
letter-spacing: 0.04em;
color: var(--color-text-primary);
margin: 0 0 0.25rem;
}
.mixer-sub {
font-size: 12px;
color: var(--color-text-tertiary);
letter-spacing: 0.1em;
text-transform: uppercase;
margin: 0 0 2rem;
}
.mixer-label {
font-size: 11px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--color-text-tertiary);
margin-bottom: 6px;
}
textarea {
width: 100%;
box-sizing: border-box;
min-height: 140px;
font-family: 'DM Mono', monospace;
font-size: 14px;
line-height: 1.6;
padding: 12px 14px;
border: 0.5px solid var(--color-border-secondary);
border-radius: var(--border-radius-md);
background: var(--color-background-secondary);
color: var(--color-text-primary);
resize: vertical;
outline: none;
transition: border-color 0.15s;
}
textarea:focus {
border-color: var(--color-border-primary);
}
textarea::placeholder {
color: var(--color-text-tertiary);
}
.controls {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
margin: 1rem 0;
}
.mode-group {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.mode-btn {
font-family: 'DM Mono', monospace;
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
padding: 5px 12px;
border: 0.5px solid var(--color-border-secondary);
border-radius: 99px;
background: transparent;
color: var(--color-text-secondary);
cursor: pointer;
transition: all 0.15s;
}
.mode-btn:hover {
background: var(--color-background-secondary);
color: var(--color-text-primary);
}
.mode-btn.active {
background: var(--color-text-primary);
color: var(--color-background-primary);
border-color: var(--color-text-primary);
}
.mix-btn {
font-family: 'Special Elite', cursive;
font-size: 15px;
letter-spacing: 0.06em;
padding: 8px 28px;
background: var(--color-text-primary);
color: var(--color-background-primary);
border: none;
border-radius: var(--border-radius-md);
cursor: pointer;
transition: opacity 0.15s, transform 0.1s;
margin-left: auto;
}
.mix-btn:hover {
opacity: 0.85;
}
.mix-btn:active {
transform: scale(0.97);
}
.param-row {
display: flex;
flex-wrap: wrap;
gap: 16px;
align-items: center;
margin-top: 8px;
}
.param-group {
display: flex;
align-items: center;
gap: 8px;
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--color-text-tertiary);
}
.param-group input[type=range] {
width: 70px;
accent-color: var(--color-text-primary);
}
.param-group input[type=number] {
width: 50px;
background: var(--color-background-secondary);
border: 0.5px solid var(--color-border-secondary);
border-radius: 4px;
color: var(--color-text-primary);
font-family: 'DM Mono', monospace;
font-size: 12px;
padding: 2px 6px;
text-align: center;
outline: none;
}
.param-group input[type=number]:focus {
border-color: var(--color-border-primary);
}
.param-val {
font-weight: 500;
color: var(--color-text-secondary);
min-width: 24px;
}
.output-area {
position: relative;
margin-top: 1.5rem;
min-height: 60px;
}
.output-box {
font-family: 'Special Elite', cursive;
font-size: 15px;
line-height: 1.85;
letter-spacing: 0.02em;
padding: 18px 20px;
border: 0.5px solid var(--color-border-tertiary);
border-radius: var(--border-radius-md);
background: var(--color-background-secondary);
color: var(--color-text-primary);
white-space: pre-wrap;
word-break: break-word;
min-height: 80px;
transition: opacity 0.25s;
}
.output-box.empty {
color: var(--color-text-tertiary);
font-family: 'DM Mono', monospace;
font-size: 13px;
font-style: italic;
}
.output-actions {
display: flex;
gap: 8px;
margin-top: 10px;
justify-content: flex-end;
}
.action-btn {
font-family: 'DM Mono', monospace;
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
padding: 5px 14px;
border: 0.5px solid var(--color-border-secondary);
border-radius: 99px;
background: transparent;
color: var(--color-text-secondary);
cursor: pointer;
transition: all 0.15s;
display: flex;
align-items: center;
gap: 5px;
}
.action-btn:hover {
background: var(--color-background-secondary);
color: var(--color-text-primary);
}
.divider {
border: none;
border-top: 0.5px solid var(--color-border-tertiary);
margin: 1.5rem 0;
}
.history-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.history-label {
font-size: 11px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--color-text-tertiary);
}
.history-clear {
font-size: 11px;
color: var(--color-text-tertiary);
cursor: pointer;
background: none;
border: none;
font-family: 'DM Mono', monospace;
letter-spacing: 0.06em;
padding: 0;
}
.history-clear:hover {
color: var(--color-text-secondary);
}
.history-item {
font-family: 'Special Elite', cursive;
font-size: 13px;
color: var(--color-text-secondary);
line-height: 1.6;
padding: 8px 12px;
border-left: 2px solid var(--color-border-tertiary);
margin-bottom: 8px;
cursor: pointer;
transition: border-color 0.15s, color 0.15s;
}
.history-item:hover {
border-color: var(--color-border-primary);
color: var(--color-text-primary);
}
.advanced-section {
display: none;
margin-top: 12px;
}
.advanced-section.visible {
display: block;
}
html.dark {
scrollbar-color: #b0ada8 #1a1a1a;
scrollbar-width: thin;
}
html.dark ::-webkit-scrollbar {
width: 8px;
height: 8px;
}
html.dark ::-webkit-scrollbar-track {
background: #1a1a1a;
border-radius: 4px;
}
html.dark ::-webkit-scrollbar-thumb {
background: #b0ada8;
border-radius: 4px;
}
html.dark ::-webkit-scrollbar-thumb:hover {
background: #e8e6e3;
}
html.dark ::-webkit-scrollbar-corner {
background: #1a1a1a;
}
.toggle-options {
font-family: 'DM Mono', monospace;
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--color-text-tertiary);
background: none;
border: none;
cursor: pointer;
padding: 0;
text-decoration: underline;
text-underline-offset: 4px;
margin-left: 8px;
}
.toggle-options:hover {
color: var(--color-text-secondary);
}
</style>
</head>
<body>
<div class="mixer-wrap">
<p class="mixer-sub">Character n‑gram · Markov chain · Cento · line & word shuffle</p>
<div class="mixer-label">Your text</div>
<textarea id="inputText" placeholder="Paste or type any text here — a paragraph, a poem, a letter, anything…"></textarea>
<div class="controls">
<div>
<div class="mixer-label" style="margin-bottom:4px;">Algorithm</div>
<div class="mode-group">
<button class="mode-btn active" data-mode="char-ngram">Char N‑gram</button>
<button class="mode-btn" data-mode="markov">Markov chain</button>
<button class="mode-btn" data-mode="cento">Cento</button>
<button class="mode-btn" data-mode="word-shuffle">Word shuffle</button>
<button class="mode-btn" data-mode="line-shuffle">Line shuffle</button>
</div>
</div>
<button class="toggle-options" id="toggleOptions" onclick="toggleAdvanced()">Options ▾</button>
<button class="mix-btn" id="mixBtn" onclick="doMix()">Mix it up ⚗</button>
</div>
<div class="advanced-section" id="advancedSection">
<div class="param-row">
<div class="param-group" id="ngramGroup">
<span>N‑gram size</span>
<input type="range" id="ngramSize" min="2" max="12" value="4" step="1">
<span class="param-val" id="ngramSizeVal">4</span>
</div>
<div class="param-group" id="outputLenGroup">
<span>Output length</span>
<input type="range" id="outputLen" min="50" max="2000" value="400" step="10">
<span class="param-val" id="outputLenVal">400</span>
</div>
<div class="param-group" id="randomnessGroup">
<span>Randomness</span>
<input type="range" id="randomness" min="0" max="100" value="60" step="1">
<span class="param-val" id="randomnessVal">60</span>
</div>
</div>
</div>
<div class="output-area">
<div class="mixer-label">Result</div>
<div class="output-box empty" id="outputBox">Your mixed text will appear here…</div>
<div class="output-actions" id="outputActions" style="display:none;">
<button class="action-btn" onclick="copyOutput()"><i class="ti ti-copy" aria-hidden="true"></i> Copy</button>
<button class="action-btn" onclick="doMix()"><i class="ti ti-refresh" aria-hidden="true"></i> Re‑mix</button>
<button class="action-btn" onclick="useAsInput()"><i class="ti ti-arrow-down" aria-hidden="true"></i> Use as input</button>
</div>
</div>
<div id="historySection" style="display:none;">
<hr class="divider">
<div class="history-row">
<span class="history-label">History</span>
<button class="history-clear" onclick="clearHistory()">Clear</button>
</div>
<div id="historyList"></div>
</div>
</div>
<script>
(function() {
function syncTheme() {
try {
if (window.parent && window.parent.document) {
const isDark = window.parent.document.documentElement.classList.contains('dark');
if (isDark) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}
} catch (e) {}
}
syncTheme();
try {
if (window.parent && window.parent.document) {
const observer = new MutationObserver(syncTheme);
observer.observe(window.parent.document.documentElement, { attributes: true, attributeFilter: ['class'] });
}
} catch (e) {}
})();
let mode = 'char-ngram';
let history = [];
document.querySelectorAll('.mode-btn').forEach(b => {
b.addEventListener('click', () => {
document.querySelectorAll('.mode-btn').forEach(x => x.classList.remove('active'));
b.classList.add('active');
mode = b.dataset.mode;
updateAdvancedVisibility();
});
});
document.getElementById('ngramSize').addEventListener('input', function() {
document.getElementById('ngramSizeVal').textContent = this.value;
});
document.getElementById('outputLen').addEventListener('input', function() {
document.getElementById('outputLenVal').textContent = this.value;
});
document.getElementById('randomness').addEventListener('input', function() {
document.getElementById('randomnessVal').textContent = this.value;
});
function updateAdvancedVisibility() {
const ngramGroup = document.getElementById('ngramGroup');
const outputLenGroup = document.getElementById('outputLenGroup');
const randomnessGroup = document.getElementById('randomnessGroup');
if (mode === 'char-ngram' || mode === 'markov') {
ngramGroup.style.display = 'flex';
outputLenGroup.style.display = 'flex';
randomnessGroup.style.display = 'flex';
} else if (mode === 'cento' || mode === 'word-shuffle' || mode === 'line-shuffle') {
ngramGroup.style.display = 'none';
outputLenGroup.style.display = 'none';
randomnessGroup.style.display = 'flex';
}
}
function toggleAdvanced() {
const section = document.getElementById('advancedSection');
const btn = document.getElementById('toggleOptions');
section.classList.toggle('visible');
btn.textContent = section.classList.contains('visible') ? 'Options ▴' : 'Options ▾';
updateAdvancedVisibility();
}
function getWords(text) {
return text.split(/\s+/).filter(w => w.length > 0);
}
function getLines(text) {
return text.split(/\n+/).map(s => s.trim()).filter(s => s.length > 0);
}
function getSentences(text) {
return text.split(/[.!?]+/).map(s => s.trim()).filter(s => s.length > 0);
}
function charNgram(text) {
const n = parseInt(document.getElementById('ngramSize').value);
const targetLen = parseInt(document.getElementById('outputLen').value);
const randomness = parseInt(document.getElementById('randomness').value) / 100;
if (text.length < n) return text;
const map = {};
for (let i = 0; i <= text.length - n; i++) {
const key = text.substring(i, i + n);
const nextChar = i + n < text.length ? text[i + n] : '';
if (!map[key]) map[key] = [];
if (nextChar) map[key].push(nextChar);
}
const keys = Object.keys(map);
if (keys.length === 0) return text;
let seed = keys[Math.floor(Math.random() * keys.length)];
let result = seed;
while (result.length < targetLen) {
const lastN = result.substring(result.length - n);
const candidates = map[lastN];
if (!candidates || candidates.length === 0) {
const randomKey = keys[Math.floor(Math.random() * keys.length)];
result += randomKey;
continue;
}
if (Math.random() < randomness) {
result += candidates[Math.floor(Math.random() * candidates.length)];
} else {
const mostCommon = candidates.sort((a, b) =>
candidates.filter(v => v === a).length - candidates.filter(v => v === b).length
).pop();
result += mostCommon;
}
}
return result;
}
function markovChain(text) {
const n = parseInt(document.getElementById('ngramSize').value);
const targetLen = parseInt(document.getElementById('outputLen').value);
const randomness = parseInt(document.getElementById('randomness').value) / 100;
const words = getWords(text);
if (words.length < n) return words.join(' ');
const map = {};
for (let i = 0; i <= words.length - n; i++) {
const key = words.slice(i, i + n).join(' ');
const nextWord = i + n < words.length ? words[i + n] : '';
if (!map[key]) map[key] = [];
if (nextWord) map[key].push(nextWord);
}
const keys = Object.keys(map);
if (keys.length === 0) return words.join(' ');
let seed = keys[Math.floor(Math.random() * keys.length)];
let resultWords = seed.split(' ');
while (resultWords.length < targetLen / 5) {
const lastN = resultWords.slice(-n).join(' ');
const candidates = map[lastN];
if (!candidates || candidates.length === 0) {
const randomKey = keys[Math.floor(Math.random() * keys.length)];
resultWords = resultWords.concat(randomKey.split(' '));
continue;
}
if (Math.random() < randomness) {
resultWords.push(candidates[Math.floor(Math.random() * candidates.length)]);
} else {
const mostCommon = candidates.sort((a, b) =>
candidates.filter(v => v === a).length - candidates.filter(v => v === b).length
).pop();
resultWords.push(mostCommon);
}
}
return resultWords.join(' ');
}
function cento(text) {
const randomness = parseInt(document.getElementById('randomness').value) / 100;
const lines = getLines(text);
if (lines.length === 0) return text;
let shuffled = lines.slice();
let swaps = Math.floor(randomness * lines.length * 2);
for (let i = 0; i < swaps; i++) {
let x = Math.floor(Math.random() * shuffled.length);
let y = Math.floor(Math.random() * shuffled.length);
[shuffled[x], shuffled[y]] = [shuffled[y], shuffled[x]];
}
return shuffled.join('\n');
}
function wordShuffle(text) {
const randomness = parseInt(document.getElementById('randomness').value) / 100;
const words = getWords(text);
if (words.length === 0) return text;
let shuffled = words.slice();
let swaps = Math.floor(randomness * words.length * 2);
for (let i = 0; i < swaps; i++) {
let x = Math.floor(Math.random() * shuffled.length);
let y = Math.floor(Math.random() * shuffled.length);
[shuffled[x], shuffled[y]] = [shuffled[y], shuffled[x]];
}
let result = [];
let lineLen = 4 + Math.floor(Math.random() * 6);
for (let i = 0; i < shuffled.length; i += lineLen) {
result.push(shuffled.slice(i, i + lineLen).join(' '));
lineLen = 4 + Math.floor(Math.random() * 6);
}
return result.join('\n');
}
function lineShuffle(text) {
const randomness = parseInt(document.getElementById('randomness').value) / 100;
const lines = getLines(text);
if (lines.length === 0) return text;
let shuffled = lines.slice();
let swaps = Math.floor(randomness * lines.length * 2);
for (let i = 0; i < swaps; i++) {
let x = Math.floor(Math.random() * shuffled.length);
let y = Math.floor(Math.random() * shuffled.length);
[shuffled[x], shuffled[y]] = [shuffled[y], shuffled[x]];
}
return shuffled.join('\n');
}
function doMix() {
const text = document.getElementById('inputText').value.trim();
if (!text) {
flashEmpty();
return;
}
let result = '';
switch (mode) {
case 'char-ngram':
result = charNgram(text);
break;
case 'markov':
result = markovChain(text);
break;
case 'cento':
result = cento(text);
break;
case 'word-shuffle':
result = wordShuffle(text);
break;
case 'line-shuffle':
result = lineShuffle(text);
break;
default:
result = text;
}
animateMixer();
showResult(result);
addToHistory(result);
}
function animateMixer() {
const btn = document.getElementById('mixBtn');
btn.style.transform = 'scale(0.95)';
setTimeout(() => { btn.style.transform = ''; }, 200);
}
function showResult(text) {
const box = document.getElementById('outputBox');
box.style.opacity = '0';
setTimeout(() => {
box.textContent = text;
box.classList.remove('empty');
box.style.opacity = '1';
document.getElementById('outputActions').style.display = 'flex';
}, 180);
}
function flashEmpty() {
const ta = document.getElementById('inputText');
ta.style.borderColor = 'var(--color-border-danger)';
setTimeout(() => ta.style.borderColor = '', 600);
}
function copyOutput() {
const text = document.getElementById('outputBox').textContent;
if (navigator.clipboard) navigator.clipboard.writeText(text);
}
function useAsInput() {
const text = document.getElementById('outputBox').textContent;
document.getElementById('inputText').value = text;
}
function addToHistory(text) {
history.unshift(text.slice(0, 120) + (text.length > 120 ? '…' : ''));
if (history.length > 5) history.pop();
renderHistory();
}
function renderHistory() {
const section = document.getElementById('historySection');
const list = document.getElementById('historyList');
if (history.length === 0) {
section.style.display = 'none';
return;
}
section.style.display = '';
list.innerHTML = '';
history.forEach((h, i) => {
const div = document.createElement('div');
div.className = 'history-item';
div.textContent = h;
div.title = 'Click to restore';
div.addEventListener('click', () => {
document.getElementById('outputBox').textContent = history[i];
document.getElementById('outputBox').classList.remove('empty');
document.getElementById('outputActions').style.display = 'flex';
});
list.appendChild(div);
});
}
function clearHistory() {
history = [];
renderHistory();
}
updateAdvancedVisibility();
</script>
<script>
(function syncTheme() {
try {
if (window.parent && window.parent !== window) {
const parentHtml = window.parent.document.documentElement;
const localHtml = document.documentElement;
const updateTheme = () => {
if (parentHtml.classList.contains('dark')) {
localHtml.classList.add('dark');
} else {
localHtml.classList.remove('dark');
}
};
updateTheme();
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === 'class') {
updateTheme();
}
});
});
observer.observe(parentHtml, { attributes: true });
return;
}
} catch (e) {
console.warn('Could not sync theme with parent window.', e);
}
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.classList.add('dark');
}
})();
</script>
</body>
</html>