feat: initial commit of markovspeed p5.js presidential speech engine
This commit is contained in:
+56
@@ -0,0 +1,56 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="cs">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>MarkovSpeed // One-Button Presidential Speech (p5.js)</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
<script src="p5.min.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="container">
|
||||||
|
<header id="header">
|
||||||
|
<div class="title-group">
|
||||||
|
<h1>MARKOVSPEED <span class="badge">1906 / 2014 / 2026</span></h1>
|
||||||
|
<div class="subtitle">ONE-BUTTON PRESIDENTIAL SPEECH ENGINE • DYNAMIC CORTICAL CADENCE</div>
|
||||||
|
</div>
|
||||||
|
<div class="controls-top">
|
||||||
|
<button id="btn-toggle" class="btn active">PAUZA / START</button>
|
||||||
|
<button id="btn-burst" class="btn burst">BURST (MEZERNÍK)</button>
|
||||||
|
<button id="btn-tts" class="btn">TTS HLAS: VYP</button>
|
||||||
|
<button id="btn-corpus" class="btn">KORPUS: PREZIDENTSKÝ</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main id="canvas-container"></main>
|
||||||
|
|
||||||
|
<footer id="footer">
|
||||||
|
<div id="hud-top">
|
||||||
|
<div class="metric">
|
||||||
|
<span class="label">RYCHLOST:</span>
|
||||||
|
<input type="range" id="speed-slider" min="1" max="60" value="12">
|
||||||
|
<span id="speed-val">12 W/S</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric">
|
||||||
|
<span class="label">KADENCE:</span>
|
||||||
|
<span id="tempo-val" class="highlight">180 WPM</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric">
|
||||||
|
<span class="label">ENTROPIE STAVŮ:</span>
|
||||||
|
<span id="entropy-val">H = 2.41 bit</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric">
|
||||||
|
<span class="label">AKTIVNÍ TOKEN:</span>
|
||||||
|
<span id="token-val" class="token-tag">—</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="ticker">
|
||||||
|
<div class="ticker-header">> GENERATORSKY VYSTUP // PREZIDENTSKY PROJEV:</div>
|
||||||
|
<div id="speech-text" class="ticker-content">Čekám na signál...</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="sketch.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,388 @@
|
|||||||
|
// =============================================================================
|
||||||
|
// MarkovSpeed // p5.js Interactive Presidential Markov Engine
|
||||||
|
// Ported & expanded from Processing (2014-2016 / 2026) MarkovSpeech.pde
|
||||||
|
// Features: Dynamic Cadence, Semantic Force-Directed Graph, Web Speech TTS
|
||||||
|
// CC0 1.0 Universal / Public Domain. Author: --kof
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
const CORPUS_PRESIDENTIAL = [
|
||||||
|
"Vážení", "spoluobčané,", "stojíme", "na", "historické", "křižovatce.",
|
||||||
|
"Naše", "země", "potřebuje", "odvahu,", "rozvahu", "a", "společenský", "smír.",
|
||||||
|
"Svoboda", "a", "demokracie", "nejsou", "samozřejmostí,", "jsou", "každodenním", "závazkem.",
|
||||||
|
"V", "této", "chvíli", "musíme", "překonat", "všechny", "vnitřní", "spory.",
|
||||||
|
"Hospodářská", "síla", "a", "technologická", "tvořivost", "tvoří", "budoucnost.",
|
||||||
|
"Důvěra", "v", "instituce", "pramení", "z", "čistého", "svědomí", "a", "poctivé", "práce.",
|
||||||
|
"Pravda", "a", "láska", "musí", "zvítězit", "nad", "lží", "a", "nenávistí.",
|
||||||
|
"Kdo", "seje", "vítr,", "sklízí", "bouři,", "ale", "naše", "vůle", "zůstává", "pevná.",
|
||||||
|
"Přeji", "vám", "pevné", "zdraví,", "naději", "a", "víru", "v", "naši", "společnou", "vlast."
|
||||||
|
];
|
||||||
|
|
||||||
|
const CORPUS_CYBERPUNK = [
|
||||||
|
"Signál", "prochází", "měděným", "vedením", "v", "rytmu", "padesáti", "hertzů.",
|
||||||
|
"Autonomní", "uzly", "vyměňují", "pakety", "bez", "dohledu", "centrálních", "brokerů.",
|
||||||
|
"V", "paměti", "tmpfs", "rotuje", "bezeztrátový", "kruhový", "buffer.",
|
||||||
|
"Svoboda", "spočívá", "v", "vlastním", "železe,", "v", "čistém", "céčku", "a", "lokálních", "modelech.",
|
||||||
|
"Žádná", "korporace", "nemůže", "vypnout", "to,", "co", "běží", "na", "baterii", "ze", "slunce.",
|
||||||
|
"Tranzistory", "chladnou,", "ale", "lidská", "paměť", "zůstává", "trvalá.",
|
||||||
|
"Přenášíme", "pravdu", "přes", "šum", "éteru", "až", "na", "konec", "noci."
|
||||||
|
];
|
||||||
|
|
||||||
|
let nodes = [];
|
||||||
|
let particles = [];
|
||||||
|
let currentState = 0;
|
||||||
|
let isRunning = true;
|
||||||
|
let isBursting = false;
|
||||||
|
let currentCorpusIdx = 0;
|
||||||
|
|
||||||
|
let baseCadence = 12; // Words per second
|
||||||
|
let stepAccumulator = 0;
|
||||||
|
let lastStepTime = 0;
|
||||||
|
|
||||||
|
let speechHistory = [];
|
||||||
|
let currentSpeechStr = "";
|
||||||
|
let totalTokensEmitted = 0;
|
||||||
|
|
||||||
|
// TTS Web Speech
|
||||||
|
let ttsEnabled = false;
|
||||||
|
let synth = null;
|
||||||
|
let czechVoice = null;
|
||||||
|
|
||||||
|
// Visual layout
|
||||||
|
let centerX, centerY;
|
||||||
|
let radius = 260;
|
||||||
|
|
||||||
|
class MarkovNode {
|
||||||
|
constructor(id, token, x, y) {
|
||||||
|
this.id = id;
|
||||||
|
this.token = token;
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.vx = 0;
|
||||||
|
this.vy = 0;
|
||||||
|
this.targets = [];
|
||||||
|
this.weights = [];
|
||||||
|
this.flashIntensity = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
addTransition(targetId, weight) {
|
||||||
|
this.targets.push(targetId);
|
||||||
|
this.weights.push(weight);
|
||||||
|
}
|
||||||
|
|
||||||
|
selectNext() {
|
||||||
|
if (this.targets.length === 0) return 0;
|
||||||
|
let total = this.weights.reduce((a, b) => a + b, 0);
|
||||||
|
let roll = random(total);
|
||||||
|
let accum = 0;
|
||||||
|
for (let i = 0; i < this.targets.length; i++) {
|
||||||
|
accum += this.weights[i];
|
||||||
|
if (roll <= accum) return this.targets[i];
|
||||||
|
}
|
||||||
|
return this.targets[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Particle {
|
||||||
|
constructor(startX, startY, targetX, targetY, targetId) {
|
||||||
|
this.x = startX;
|
||||||
|
this.y = startY;
|
||||||
|
this.targetX = targetX;
|
||||||
|
this.targetY = targetY;
|
||||||
|
this.targetId = targetId;
|
||||||
|
this.progress = 0.0;
|
||||||
|
this.speed = random(0.08, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
this.progress += this.speed;
|
||||||
|
return this.progress >= 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let px = lerp(this.x, this.targetX, this.progress);
|
||||||
|
let py = lerp(this.y, this.targetY, this.progress);
|
||||||
|
noStroke();
|
||||||
|
fill(255, 204, 17, (1.0 - this.progress) * 255);
|
||||||
|
circle(px, py, 5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildModel(vocab) {
|
||||||
|
nodes = [];
|
||||||
|
particles = [];
|
||||||
|
currentState = 0;
|
||||||
|
let total = vocab.length;
|
||||||
|
|
||||||
|
for (let i = 0; i < total; i++) {
|
||||||
|
// Spatial layout: Fibonacci phyllotaxis spiral
|
||||||
|
let angle = i * 2.39996323; // Golden angle
|
||||||
|
let r = sqrt(i / total) * radius;
|
||||||
|
let x = centerX + cos(angle) * r;
|
||||||
|
let y = centerY + sin(angle) * r;
|
||||||
|
nodes.push(new MarkovNode(i, vocab[i], x, y));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate transition probabilities
|
||||||
|
for (let i = 0; i < total; i++) {
|
||||||
|
let n = nodes[i];
|
||||||
|
// Main sequential train
|
||||||
|
if (i + 1 < total) {
|
||||||
|
n.addTransition(i + 1, 0.65);
|
||||||
|
} else {
|
||||||
|
n.addTransition(0, 0.70); // loop
|
||||||
|
}
|
||||||
|
|
||||||
|
// Branching rhetorical attributions
|
||||||
|
let b1 = (i + 3) % total;
|
||||||
|
let b2 = (i + 7) % total;
|
||||||
|
let b3 = (i + 13) % total;
|
||||||
|
n.addTransition(b1, 0.18);
|
||||||
|
n.addTransition(b2, 0.11);
|
||||||
|
n.addTransition(b3, 0.06);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setup() {
|
||||||
|
let container = document.getElementById("canvas-container");
|
||||||
|
let canvas = createCanvas(container.clientWidth, container.clientHeight);
|
||||||
|
canvas.parent("canvas-container");
|
||||||
|
|
||||||
|
centerX = width / 2;
|
||||||
|
centerY = height / 2;
|
||||||
|
radius = min(width, height) * 0.40;
|
||||||
|
|
||||||
|
buildModel(CORPUS_PRESIDENTIAL);
|
||||||
|
|
||||||
|
initTTS();
|
||||||
|
initUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
function windowResized() {
|
||||||
|
let container = document.getElementById("canvas-container");
|
||||||
|
resizeCanvas(container.clientWidth, container.clientHeight);
|
||||||
|
centerX = width / 2;
|
||||||
|
centerY = height / 2;
|
||||||
|
radius = min(width, height) * 0.40;
|
||||||
|
buildModel(currentCorpusIdx === 0 ? CORPUS_PRESIDENTIAL : CORPUS_CYBERPUNK);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initTTS() {
|
||||||
|
if ("speechSynthesis" in window) {
|
||||||
|
synth = window.speechSynthesis;
|
||||||
|
let loadVoices = () => {
|
||||||
|
let voices = synth.getVoices();
|
||||||
|
czechVoice = voices.find(v => v.lang.startsWith("cs")) || voices[0];
|
||||||
|
};
|
||||||
|
loadVoices();
|
||||||
|
if (synth.onvoiceschanged !== undefined) {
|
||||||
|
synth.onvoiceschanged = loadVoices;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function speakWord(word) {
|
||||||
|
if (!ttsEnabled || !synth) return;
|
||||||
|
// Clean punctuation
|
||||||
|
let clean = word.replace(/[.,!?;:]/g, "");
|
||||||
|
if (!clean) return;
|
||||||
|
let utter = new SpeechSynthesisUtterance(clean);
|
||||||
|
if (czechVoice) utter.voice = czechVoice;
|
||||||
|
utter.lang = "cs-CZ";
|
||||||
|
// Adjust rate to current speed
|
||||||
|
let rate = map(baseCadence, 1, 60, 0.85, 2.5);
|
||||||
|
utter.rate = constrain(rate, 0.5, 3.0);
|
||||||
|
utter.pitch = 1.0;
|
||||||
|
utter.volume = 0.9;
|
||||||
|
synth.speak(utter);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initUI() {
|
||||||
|
const btnToggle = document.getElementById("btn-toggle");
|
||||||
|
btnToggle.addEventListener("click", () => {
|
||||||
|
isRunning = !isRunning;
|
||||||
|
btnToggle.classList.toggle("active", isRunning);
|
||||||
|
btnToggle.textContent = isRunning ? "PAUZA / START" : "ZASTAVENO";
|
||||||
|
});
|
||||||
|
|
||||||
|
const btnBurst = document.getElementById("btn-burst");
|
||||||
|
btnBurst.addEventListener("mousedown", () => { isBursting = true; });
|
||||||
|
btnBurst.addEventListener("mouseup", () => { isBursting = false; });
|
||||||
|
btnBurst.addEventListener("mouseleave", () => { isBursting = false; });
|
||||||
|
btnBurst.addEventListener("touchstart", (e) => { e.preventDefault(); isBursting = true; });
|
||||||
|
btnBurst.addEventListener("touchend", () => { isBursting = false; });
|
||||||
|
|
||||||
|
const btnTTS = document.getElementById("btn-tts");
|
||||||
|
btnTTS.addEventListener("click", () => {
|
||||||
|
ttsEnabled = !ttsEnabled;
|
||||||
|
btnTTS.classList.toggle("active", ttsEnabled);
|
||||||
|
btnTTS.textContent = ttsEnabled ? "TTS HLAS: ZAP" : "TTS HLAS: VYP";
|
||||||
|
if (ttsEnabled && synth) {
|
||||||
|
speakWord("Techné aktivováno");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const btnCorpus = document.getElementById("btn-corpus");
|
||||||
|
btnCorpus.addEventListener("click", () => {
|
||||||
|
currentCorpusIdx = 1 - currentCorpusIdx;
|
||||||
|
let isPres = (currentCorpusIdx === 0);
|
||||||
|
btnCorpus.textContent = isPres ? "KORPUS: PREZIDENTSKÝ" : "KORPUS: CYBERPUNK";
|
||||||
|
buildModel(isPres ? CORPUS_PRESIDENTIAL : CORPUS_CYBERPUNK);
|
||||||
|
currentSpeechStr = "> ";
|
||||||
|
updateTicker();
|
||||||
|
});
|
||||||
|
|
||||||
|
const speedSlider = document.getElementById("speed-slider");
|
||||||
|
const speedVal = document.getElementById("speed-val");
|
||||||
|
speedSlider.addEventListener("input", (e) => {
|
||||||
|
baseCadence = parseInt(e.target.value);
|
||||||
|
speedVal.textContent = baseCadence + " W/S";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function keyPressed() {
|
||||||
|
if (key === " ") {
|
||||||
|
isBursting = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function keyReleased() {
|
||||||
|
if (key === " ") {
|
||||||
|
isBursting = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitToken() {
|
||||||
|
if (nodes.length === 0) return;
|
||||||
|
|
||||||
|
let current = nodes[currentState];
|
||||||
|
current.flashIntensity = 1.0;
|
||||||
|
totalTokensEmitted++;
|
||||||
|
|
||||||
|
let token = current.token;
|
||||||
|
speakWord(token);
|
||||||
|
|
||||||
|
// Update speech text
|
||||||
|
currentSpeechStr += token + " ";
|
||||||
|
if (currentSpeechStr.length > 280) {
|
||||||
|
currentSpeechStr = "> ..." + currentSpeechStr.substring(currentSpeechStr.length - 220);
|
||||||
|
}
|
||||||
|
updateTicker(token);
|
||||||
|
|
||||||
|
// Pick next state
|
||||||
|
let nextState = current.selectNext();
|
||||||
|
let nextNode = nodes[nextState];
|
||||||
|
|
||||||
|
// Spawn visual packet particle
|
||||||
|
particles.push(new Particle(current.x, current.y, nextNode.x, nextNode.y, nextState));
|
||||||
|
currentState = nextState;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTicker(lastToken = "") {
|
||||||
|
let elem = document.getElementById("speech-text");
|
||||||
|
if (elem) elem.textContent = currentSpeechStr;
|
||||||
|
|
||||||
|
let tag = document.getElementById("token-val");
|
||||||
|
if (tag && lastToken) tag.textContent = lastToken;
|
||||||
|
|
||||||
|
let tempo = document.getElementById("tempo-val");
|
||||||
|
let effectiveCadence = isBursting ? baseCadence * 3 : baseCadence;
|
||||||
|
if (tempo) tempo.textContent = (effectiveCadence * 60) + " WPM";
|
||||||
|
|
||||||
|
let entropy = document.getElementById("entropy-val");
|
||||||
|
if (entropy) {
|
||||||
|
let curr = nodes[currentState];
|
||||||
|
let H = curr ? (Math.log2(curr.targets.length) || 1.0).toFixed(2) : "2.41";
|
||||||
|
entropy.textContent = `H = ${H} bit`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw() {
|
||||||
|
background(5, 8, 12, 45); // Phosphor persistence decay
|
||||||
|
|
||||||
|
// Center subtle cortical oscillation ring
|
||||||
|
let pulse = sin(frameCount * 0.05) * 6;
|
||||||
|
noFill();
|
||||||
|
stroke(26, 42, 58, 80);
|
||||||
|
circle(centerX, centerY, (radius * 2) + pulse);
|
||||||
|
|
||||||
|
// 1. Draw static and active transition lines
|
||||||
|
for (let i = 0; i < nodes.length; i++) {
|
||||||
|
let n = nodes[i];
|
||||||
|
for (let tId of n.targets) {
|
||||||
|
let target = nodes[tId];
|
||||||
|
if (!target) continue;
|
||||||
|
|
||||||
|
let isCurr = (i === currentState);
|
||||||
|
if (isCurr) {
|
||||||
|
stroke(255, 204, 17, 180);
|
||||||
|
strokeWeight(1.5);
|
||||||
|
} else {
|
||||||
|
stroke(20, 36, 50, 40);
|
||||||
|
strokeWeight(0.7);
|
||||||
|
}
|
||||||
|
line(n.x, n.y, target.x, target.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Update and draw particles
|
||||||
|
for (let i = particles.length - 1; i >= 0; i--) {
|
||||||
|
let p = particles[i];
|
||||||
|
p.render();
|
||||||
|
if (p.update()) {
|
||||||
|
particles.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Draw nodes & labels
|
||||||
|
for (let i = 0; i < nodes.length; i++) {
|
||||||
|
let n = nodes[i];
|
||||||
|
n.flashIntensity *= 0.91; // Decay
|
||||||
|
|
||||||
|
let isCurrent = (i === currentState);
|
||||||
|
let glow = n.flashIntensity;
|
||||||
|
|
||||||
|
if (isCurrent || glow > 0.05) {
|
||||||
|
fill(255, 204, 17, 180 + glow * 75);
|
||||||
|
stroke(255, 230, 80, glow * 255);
|
||||||
|
strokeWeight(2);
|
||||||
|
circle(n.x, n.y, 10 + glow * 8);
|
||||||
|
|
||||||
|
// Glowing text label
|
||||||
|
noStroke();
|
||||||
|
fill(255, 240, 160);
|
||||||
|
textSize(12);
|
||||||
|
textAlign(CENTER, BOTTOM);
|
||||||
|
text(n.token, n.x, n.y - 10);
|
||||||
|
} else {
|
||||||
|
fill(35, 55, 75);
|
||||||
|
noStroke();
|
||||||
|
circle(n.x, n.y, 4);
|
||||||
|
|
||||||
|
fill(90, 120, 145, 160);
|
||||||
|
textSize(9);
|
||||||
|
textAlign(CENTER, TOP);
|
||||||
|
text(n.token, n.x, n.y + 6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Cadence timing engine
|
||||||
|
if (isRunning) {
|
||||||
|
let effectiveCadence = isBursting ? baseCadence * 3 : baseCadence;
|
||||||
|
let intervalSec = 1.0 / effectiveCadence;
|
||||||
|
let now = millis() / 1000.0;
|
||||||
|
|
||||||
|
if (lastStepTime === 0) lastStepTime = now;
|
||||||
|
let dt = now - lastStepTime;
|
||||||
|
lastStepTime = now;
|
||||||
|
stepAccumulator += dt;
|
||||||
|
|
||||||
|
if (stepAccumulator >= intervalSec) {
|
||||||
|
emitToken();
|
||||||
|
stepAccumulator = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lastStepTime = millis() / 1000.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #05080c;
|
||||||
|
color: #e0e8f0;
|
||||||
|
font-family: "IBM Plex Mono", "Terminus", "Courier New", monospace;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
#header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 18px;
|
||||||
|
background: #090e14;
|
||||||
|
border-bottom: 2px solid #1a2634;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-group h1 {
|
||||||
|
font-size: 1.15rem;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
color: #ffcc11;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-group .badge {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
background: #1e2d3d;
|
||||||
|
color: #00d4ff;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-left: 8px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 0.68rem;
|
||||||
|
color: #7b93a8;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls-top {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
background: #0d1520;
|
||||||
|
border: 1px solid #2a3b4c;
|
||||||
|
color: #8da4be;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 6px 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background: #182535;
|
||||||
|
color: #ffffff;
|
||||||
|
border-color: #ffcc11;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.active {
|
||||||
|
background: #122822;
|
||||||
|
border-color: #33ff88;
|
||||||
|
color: #33ff88;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.burst {
|
||||||
|
background: #2b220d;
|
||||||
|
border-color: #ffaa00;
|
||||||
|
color: #ffcc11;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.burst:active {
|
||||||
|
background: #ffcc11;
|
||||||
|
color: #05080c;
|
||||||
|
}
|
||||||
|
|
||||||
|
#canvas-container {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #05080c;
|
||||||
|
}
|
||||||
|
|
||||||
|
#canvas-container canvas {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
#footer {
|
||||||
|
background: #090e14;
|
||||||
|
border-top: 2px solid #1a2634;
|
||||||
|
padding: 10px 18px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
#hud-top {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px dashed #1a2634;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric .label {
|
||||||
|
color: #61788d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric .highlight {
|
||||||
|
color: #33ff88;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric .token-tag {
|
||||||
|
color: #ffcc11;
|
||||||
|
font-weight: bold;
|
||||||
|
background: #1f1d10;
|
||||||
|
padding: 1px 8px;
|
||||||
|
border: 1px solid #ffcc1144;
|
||||||
|
}
|
||||||
|
|
||||||
|
#speed-slider {
|
||||||
|
accent-color: #ffcc11;
|
||||||
|
width: 110px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#speed-val {
|
||||||
|
color: #ffcc11;
|
||||||
|
min-width: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ticker {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticker-header {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
color: #ffaa00;
|
||||||
|
letter-spacing: 1.5px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticker-content {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.45;
|
||||||
|
color: #d8e8f8;
|
||||||
|
min-height: 48px;
|
||||||
|
max-height: 68px;
|
||||||
|
overflow-y: auto;
|
||||||
|
word-break: break-word;
|
||||||
|
background: #040608;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-left: 3px solid #ffcc11;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticker-content::after {
|
||||||
|
content: "█";
|
||||||
|
color: #ffcc11;
|
||||||
|
animation: blink 0.8s infinite;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blink {
|
||||||
|
0%, 49% { opacity: 1; }
|
||||||
|
50%, 100% { opacity: 0; }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user