<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TRIDICE — ESSENTIAL ROLL</title>
<link rel="preconnect" href="
fonts.googleapis.com">
<link rel="preconnect" href="
fonts.gstatic.com" crossorigin>
<link href="
fonts.googleapis.com/css2?fa…" rel="stylesheet">
<style>
:root{
/* iridescent / holographic palette — all HSLA */
--void: hsla(232, 60%, 4%, 1);
--void-2: hsla(228, 55%, 8%, 1);
--panel: hsla(225, 45%, 9%, 0.72);
--panel-edge: hsla(210, 80%, 70%, 0.22);
--lime: hsla(80, 95%, 58%, 1);
--lime-soft: hsla(80, 95%, 58%, 0.16);
--magenta: hsla(322, 95%, 60%, 1);
--cyan: hsla(180, 90%, 62%, 1);
--text: hsla(200, 30%, 92%, 1);
--text-dim: hsla(205, 25%, 70%, 0.62);
--chrome-1: hsla(190, 80%, 88%, 1);
--chrome-2: hsla(215, 35%, 55%, 1);
}
*{ box-sizing:border-box; margin:0; padding:0; }
html{ height:100%; }
body{
font-family:'Chakra Petch', sans-serif;
color:var(--text);
min-height:100vh;
display:flex;
align-items:center;
justify-content:center;
padding:24px;
overflow-x:hidden;
overflow-y:auto;
background:
radial-gradient(140% 120% at 82% -10%, hsla(168, 85%, 55%, 0.30) 0%, hsla(168,85%,55%,0) 42%),
radial-gradient(120% 130% at 8% 110%, hsla(322, 92%, 52%, 0.34) 0%, hsla(322,92%,52%,0) 45%),
linear-gradient(165deg, var(--void-2) 0%, var(--void) 55%, hsla(248,55%,7%,1) 100%);
}
/* holographic foil sweep behind everything */
body::before{
content:"";
position:fixed; inset:-30%;
z-index:0;
background:linear-gradient(
115deg,
hsla(0,0%,100%,0) 0%,
hsla(300,90%,65%,0.10) 18%,
hsla(190,90%,60%,0.12) 33%,
hsla(80,90%,60%,0.10) 47%,
hsla(40,95%,60%,0.10) 60%,
hsla(330,90%,62%,0.12) 74%,
hsla(0,0%,100%,0) 100%
);
background-size:300% 300%;
mix-blend-mode:screen;
filter:blur(2px);
animation:foil 14s ease-in-out infinite alternate;
}
@keyframes foil{
0%{ background-position:0% 50%; transform:rotate(0deg) scale(1); }
100%{ background-position:100% 50%; transform:rotate(0.5deg) scale(1.04); }
}
/* faint scanline grain for the sci-fi sleeve feel */
body::after{
content:"";
position:fixed; inset:0; z-index:1; pointer-events:none;
background:repeating-linear-gradient(
0deg,
hsla(200,40%,80%,0.025) 0px,
hsla(200,40%,80%,0.025) 1px,
hsla(0,0%,0%,0) 2px,
hsla(0,0%,0%,0) 4px
);
}
.console{
position:relative;
z-index:2;
width:min(640px, 100%);
background:var(--panel);
backdrop-filter:blur(14px) saturate(1.2);
-webkit-backdrop-filter:blur(14px) saturate(1.2);
border:1px solid var(--panel-edge);
border-radius:14px;
padding:26px 28px 24px;
box-shadow:
0 0 0 1px hsla(0,0%,100%,0.04),
0 30px 80px hsla(240,80%,3%,0.7),
inset 0 1px 0 hsla(0,0%,100%,0.08);
}
/* header bar echoing the BBC RADIO / EVIANCHRIST strip */
.head{
display:flex; align-items:baseline; justify-content:space-between;
gap:12px; flex-wrap:wrap;
padding-bottom:14px; margin-bottom:18px;
border-bottom:1px solid hsla(200,40%,70%,0.14);
}
.title{
font-weight:700;
font-size:clamp(20px, 5vw, 30px);
letter-spacing:0.22em;
text-transform:uppercase;
background:linear-gradient(92deg,
var(--chrome-1) 0%,
var(--chrome-2) 28%,
var(--chrome-1) 50%,
var(--magenta) 72%,
var(--cyan) 100%);
background-size:200% auto;
-webkit-background-clip:text; background-clip:text;
color:transparent;
animation:shine 6s linear infinite;
text-shadow:0 0 22px hsla(190,90%,70%,0.10);
}
@keyframes shine{ to{ background-position:200% center; } }
.tag{
font-family:'Space Mono', monospace;
font-size:10px; letter-spacing:0.26em; text-transform:uppercase;
color:var(--void);
background:var(--lime);
padding:3px 9px; border-radius:3px;
box-shadow:0 0 18px hsla(80,95%,58%,0.45);
white-space:nowrap;
}
.sub{
width:100%;
font-family:'Space Mono', monospace;
font-style:italic;
font-size:11px; letter-spacing:0.08em;
color:var(--text-dim);
margin-top:6px;
}
/* dice tray */
.tray{
display:flex; gap:18px; justify-content:center;
margin:8px 0 22px;
}
.die{
--face: hsla(220,40%,14%,0.9);
width:84px; height:84px;
border-radius:16px;
position:relative;
display:grid;
grid-template-columns:repeat(3,1fr);
grid-template-rows:repeat(3,1fr);
padding:13px;
background:
linear-gradient(150deg, hsla(190,80%,75%,0.16), hsla(322,80%,60%,0.10) 40%, hsla(80,80%,60%,0.10) 70%, hsla(220,40%,14%,0.0)),
var(--face);
border:1px solid hsla(200,70%,80%,0.30);
box-shadow:
inset 0 1px 0 hsla(0,0%,100%,0.18),
inset 0 -10px 22px hsla(240,70%,4%,0.6),
0 10px 26px hsla(240,80%,3%,0.55);
transition:transform 0.12s ease;
}
.die.rolling{ animation:tumble 0.6s cubic-bezier(.36,.07,.19,.97); }
@keyframes tumble{
0%{ transform:rotate(0) scale(1); }
25%{ transform:rotate(-18deg) scale(1.08) translateY(-6px); }
55%{ transform:rotate(14deg) scale(0.96) translateY(2px); }
80%{ transform:rotate(-6deg) scale(1.03); }
100%{ transform:rotate(0) scale(1); }
}
.pip{
align-self:center; justify-self:center;
width:13px; height:13px; border-radius:50%;
background:radial-gradient(circle at 35% 30%, var(--chrome-1), var(--magenta) 55%, var(--cyan) 100%);
box-shadow:0 0 9px hsla(300,90%,70%,0.55), inset 0 0 3px hsla(0,0%,100%,0.7);
opacity:0; transform:scale(0.3);
transition:opacity 0.18s, transform 0.18s;
}
.pip.on{ opacity:1; transform:scale(1); }
/* pip grid positions (1..9 cells) */
.p1{ grid-area:1/1; } .p2{ grid-area:1/3; }
.p3{ grid-area:2/2; }
.p4{ grid-area:2/1; } .p5{ grid-area:2/3; }
.p6{ grid-area:1/2; } .p7{ grid-area:3/2; }
.p8{ grid-area:3/1; } .p9{ grid-area:3/3; }
/* roll button */
.roll{
display:block; width:100%;
font-family:'Chakra Petch', sans-serif;
font-weight:700; font-size:16px;
letter-spacing:0.34em; text-transform:uppercase;
color:var(--void);
padding:15px;
border:none; border-radius:10px; cursor:pointer;
background:linear-gradient(100deg, var(--lime) 0%, hsla(150,90%,60%,1) 45%, var(--cyan) 100%);
background-size:180% auto;
box-shadow:0 0 30px hsla(95,90%,55%,0.35), inset 0 1px 0 hsla(0,0%,100%,0.4);
transition:background-position 0.4s ease, transform 0.08s ease, box-shadow 0.3s;
}
.roll:hover{ background-position:100% center; box-shadow:0 0 44px hsla(150,90%,58%,0.5); }
.roll:active{ transform:translateY(2px) scale(0.99); }
.roll:disabled{ opacity:0.5; cursor:wait; }
.roll.done{
background:linear-gradient(100deg, var(--magenta) 0%, hsla(280,90%,62%,1) 50%, var(--cyan) 100%);
box-shadow:0 0 30px hsla(322,90%,58%,0.4), inset 0 1px 0 hsla(0,0%,100%,0.4);
}
.roll.done:hover{ box-shadow:0 0 44px hsla(322,90%,60%,0.55); }
/* stats row */
.stats{
display:flex; justify-content:space-between; gap:10px;
font-family:'Space Mono', monospace;
font-size:11px; letter-spacing:0.12em;
color:var(--text-dim);
margin:18px 2px 8px;
text-transform:uppercase;
}
.stats b{ color:var(--lime); font-weight:400; }
/* nonogram grid */
.nono-wrap{
border:1px solid hsla(200,40%,70%,0.14);
border-radius:10px;
background:hsla(228,55%,5%,0.55);
margin:18px 0 0;
overflow:hidden;
}
.nono-head{
display:flex; align-items:center; justify-content:space-between; gap:10px;
font-family:'Space Mono', monospace;
font-size:10px; letter-spacing:0.24em; text-transform:uppercase;
color:var(--text-dim);
padding:9px 14px;
background:hsla(228,55%,6%,0.92);
border-bottom:1px solid hsla(200,40%,70%,0.12);
}
.nono-head .fill{ color:var(--lime); font-style:italic; }
.grid{
display:grid;
grid-template-columns:repeat(18, 1fr);
gap:2px;
padding:12px;
}
.cell{
aspect-ratio:1 / 1;
border-radius:2px;
background:hsla(220,40%,16%,0.5);
box-shadow:inset 0 0 0 1px hsla(200,40%,70%,0.05);
transition:background 0.35s ease, box-shadow 0.35s ease, transform 0.35s cubic-bezier(.36,.07,.19,.97);
}
/* filled-cell pop-in */
.cell.on{ animation:pop 0.4s cubic-bezier(.36,.07,.19,.97) both; }
@keyframes pop{
0%{ transform:scale(0.2) rotate(-12deg); opacity:0; }
60%{ transform:scale(1.18) rotate(4deg); }
100%{ transform:scale(1) rotate(0); opacity:1; }
}
.log-wrap{
border:1px solid hsla(200,40%,70%,0.14);
border-radius:10px;
background:hsla(228,55%,5%,0.55);
max-height:220px; overflow-y:auto;
}
.log-head{
position:sticky; top:0;
font-family:'Space Mono', monospace;
font-size:10px; letter-spacing:0.24em; text-transform:uppercase;
color:var(--text-dim);
padding:9px 14px;
background:hsla(228,55%,6%,0.92);
border-bottom:1px solid hsla(200,40%,70%,0.12);
backdrop-filter:blur(4px);
}
.log{ list-style:none; padding:6px 0; }
.log li{
display:flex; align-items:center; gap:12px;
font-family:'Space Mono', monospace;
font-size:13px; letter-spacing:0.04em;
padding:6px 14px;
color:var(--text);
border-left:2px solid transparent;
animation:slideIn 0.32s ease both;
}
.log li:hover{ background:hsla(200,60%,60%,0.05); border-left-color:var(--magenta); }
@keyframes slideIn{ from{ opacity:0; transform:translateX(-8px); } to{ opacity:1; transform:none; } }
.idx{ color:hsla(322,90%,68%,0.85); font-style:italic; min-width:38px; }
.tuple{
background:linear-gradient(92deg, var(--chrome-1), var(--cyan));
-webkit-background-clip:text; background-clip:text; color:transparent;
font-weight:700;
}
.sum{ margin-left:auto; color:var(--lime); font-size:11px; }
.empty{
padding:20px 14px; text-align:center;
font-family:'Space Mono', monospace; font-style:italic;
font-size:12px; color:var(--text-dim);
}
/* scrollbar */
.log-wrap::-webkit-scrollbar{ width:8px; }
.log-wrap::-webkit-scrollbar-thumb{
background:linear-gradient(hsla(322,90%,60%,0.6), hsla(180,90%,62%,0.6));
border-radius:8px;
}
.log-wrap::-webkit-scrollbar-track{ background:transparent; }
</style>
</head>
<body>
<main class="console">
<header class="head">
<span class="title">TriDice</span>
<span class="tag">Essential Roll</span>
<span class="sub">three-die sequencer — logged as (d1, d2, d3)</span>
</header>
<section class="tray" id="tray">
<div class="die" id="die-0"></div>
<div class="die" id="die-1"></div>
<div class="die" id="die-2"></div>
</section>
<button class="roll" id="rollBtn">Roll</button>
<div class="stats">
<span>Rolls <b id="count">000</b></span>
<span>Last sum <b id="lastSum">—</b></span>
<span>Avg sum <b id="avgSum">—</b></span>
</div>
<div class="nono-wrap">
<div class="nono-head">
<span>Nonogram // 18×18</span>
<span>Fill <b class="fill" id="nonoFill">0%</b></span>
</div>
<div class="grid" id="grid"></div>
</div>
<div class="log-wrap">
<div class="log-head">Running Log</div>
<ul class="log" id="log">
<li class="empty" id="emptyState">awaiting first roll…</li>
</ul>
</div>
</main>
<script>
// pip layouts: which cell positions light up for each die value
var LAYOUTS = {
1: ['p3'],
2: ['p1','p9'],
3: ['p1','p3','p9'],
4: ['p1','p2','p8','p9'],
5: ['p1','p2','p3','p8','p9'],
6: ['p1','p2','p4','p5','p8','p9']
};
var dice = [
document.getElementById('die-0'),
document.getElementById('die-1'),
document.getElementById('die-2')
];
var rollBtn = document.getElementById('rollBtn');
var logEl = document.getElementById('log');
var emptyState= document.getElementById('emptyState');
var countEl = document.getElementById('count');
var lastSumEl = document.getElementById('lastSum');
var avgSumEl = document.getElementById('avgSum');
var gridEl = document.getElementById('grid');
var nonoFillEl= document.getElementById('nonoFill');
var ROWS = 18;
var COLS = 18;
var TOTAL = ROWS * COLS; // 324 cells in the stream
var cells = []; // flat array of cell elements, row-major
var cursor = 0; // next free cell in the continuous stream
var gridFull = false;
var results = []; // array of [d1,d2,d3] tuples
var sumTotal = 0;
// build the 9 pip slots inside each die once
function buildDie(el){
for (var i = 1; i <= 9; i ){
var pip = document.createElement('span');
pip.className = 'pip p' i;
el.appendChild(pip);
}
}
dice.forEach(buildDie);
// build the 18x18 grid once — agnostic cells, no per-die zoning
function buildGrid(){
for (var i = 0; i < TOTAL; i ){
var cell = document.createElement('div');
cell.className = 'cell';
gridEl.appendChild(cell);
cells.push(cell);
}
}
buildGrid();
// light up one cell (flat index) with a randomized iridescent color at hue h
function fillCell(idx, h){
var cell = cells[idx];
cell.style.background =
'radial-gradient(circle at 32% 28%, hsla(' h ',95%,82%,1), hsla(' h ',92%,58%,1) 60%, hsla(' ((h 40) % 360) ',90%,50%,1) 100%)';
cell.style.boxShadow =
'0 0 9px hsla(' h ',95%,65%,0.6), inset 0 0 3px hsla(0,0%,100%,0.55)';
cell.classList.remove('on');
void cell.offsetWidth; // restart pop animation
cell.classList.add('on');
}
// place this roll's three runs into the continuous stream. Each die value is a
// run of that many filled cells; every run is followed by exactly ONE empty
// gap. Runs flow cell-to-cell and wrap across rows — the grid is agnostic to
// which die produced them, so coverage is intentionally uneven.
function placeRoll(values){
for (var d = 0; d < 3; d ){
var blocks = values[d];
var baseHue = Math.floor(Math.random() * 360);
for (var i = 0; i < blocks; i ){
if (cursor >= TOTAL) { gridFull = true; break; }
fillCell(cursor, (baseHue i * 6) % 360); // gentle drift within run
cursor ;
}
cursor ; // single empty gap after the run
if (cursor >= TOTAL) { gridFull = true; break; }
}
var pct = Math.min(100, Math.round((cursor / TOTAL) * 100));
nonoFillEl.textContent = pct '%';
}
function renderDie(el, value){
var pips = el.querySelectorAll('.pip');
pips.forEach(function(p){ p.classList.remove('on'); });
LAYOUTS[value].forEach(function(cls){
el.querySelector('.' cls).classList.add('on');
});
}
function pad(n){ return String(n).padStart(3, '0'); }
function roll(){
rollBtn.disabled = true;
var values = [];
dice.forEach(function(el){
el.classList.remove('rolling');
void el.offsetWidth; // restart animation
el.classList.add('rolling');
values.push(1 Math.floor(Math.random() * 6));
});
// settle dice mid-tumble so pips land with the bounce
setTimeout(function(){
dice.forEach(function(el, i){ renderDie(el, values[i]); });
}, 300);
setTimeout(function(){
record(values);
if (gridFull){
rollBtn.textContent = 'Reset Grid';
rollBtn.classList.add('done');
}
rollBtn.disabled = false;
}, 600);
}
function reset(){
results = [];
sumTotal = 0;
cursor = 0;
gridFull = false;
cells.forEach(function(cell){
cell.classList.remove('on');
cell.style.background = '';
cell.style.boxShadow = '';
});
logEl.innerHTML = '';
emptyState = document.createElement('li');
emptyState.className = 'empty';
emptyState.id = 'emptyState';
emptyState.textContent = 'awaiting first roll…';
logEl.appendChild(emptyState);
countEl.textContent = '000';
lastSumEl.textContent = '—';
avgSumEl.textContent = '—';
nonoFillEl.textContent = '0%';
rollBtn.textContent = 'Roll';
rollBtn.classList.remove('done');
}
function record(values){
results.push(values);
var sum = values[0] values[1] values[2];
sumTotal = sum;
placeRoll(values); // stream this roll's runs into the grid
if (emptyState) { emptyState.remove(); emptyState = null; }
var li = document.createElement('li');
li.innerHTML =
'<span class="idx">#' pad(results.length) '</span>'
'<span class="tuple">(' values.join(', ') ')</span>'
'<span class="sum">Σ ' sum '</span>';
logEl.insertBefore(li, logEl.firstChild); // newest on top
countEl.textContent = pad(results.length);
lastSumEl.textContent = sum;
avgSumEl.textContent = (sumTotal / results.length).toFixed(1);
}
rollBtn.addEventListener('click', function(){
if (gridFull) reset();
else roll();
});
</script>
</body>
</html>