tinyweb/themes/kodama.html
Derick Phan 9c4ed9ac9e
Add themes folder with kodama template and gitignore index.db
Save the custom kodama template to themes/kodama.html so it's
version-controlled as a file rather than only living in the database.
Stop tracking index.db since it's runtime data, not source code.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-26 10:11:32 -07:00

746 lines
No EOL
23 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans:ital,wght@0,400;0,500;0,600;1,400&display=swap');
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'IBM Plex Sans', -apple-system, sans-serif;
font-size: 16px;
line-height: 1.65;
color: #c8c8c8;
background: #111;
cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20'%3E%3Ccircle cx='10' cy='10' r='3' fill='none' stroke='%23888' stroke-width='1.5'/%3E%3Ccircle cx='10' cy='10' r='1' fill='%23aaa'/%3E%3C/svg%3E") 10 10, default;
}
a, button, input[type="submit"], summary, label {
cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20'%3E%3Ccircle cx='10' cy='10' r='6' fill='none' stroke='%23fff' stroke-width='1' opacity='0.6'/%3E%3Ccircle cx='10' cy='10' r='1.5' fill='%23fff'/%3E%3C/svg%3E") 10 10, pointer;
}
input, textarea {
cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20'%3E%3Cline x1='10' y1='3' x2='10' y2='17' stroke='%23aaa' stroke-width='1.5'/%3E%3C/svg%3E") 10 10, text;
}
/* scanline overlay */
body::before {
content: '';
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
background: repeating-linear-gradient(
0deg,
transparent,
transparent 2px,
rgba(255, 255, 255, 0.008) 2px,
rgba(255, 255, 255, 0.008) 4px
);
pointer-events: none;
z-index: 1000;
}
/* vignette */
body::after {
content: '';
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
background: radial-gradient(ellipse at center, transparent 60%, rgba(0, 0, 0, 0.4) 100%);
pointer-events: none;
z-index: 999;
}
/* floating particles canvas */
#particles {
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
pointer-events: none;
z-index: 0;
}
/* cursor trail canvas */
#trail {
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
pointer-events: none;
z-index: 998;
}
/* kodama spirits */
#kodama {
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
pointer-events: none;
z-index: 2;
}
.shell {
max-width: 660px;
margin: 0 auto;
padding: 0 1.5rem;
position: relative;
z-index: 1;
}
/* nav */
nav {
display: flex;
align-items: baseline;
justify-content: space-between;
padding: 1.5rem 0 1.2rem;
border-bottom: 1px solid #232323;
}
nav .site {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.85rem;
font-weight: 500;
color: #e8e8e8;
text-decoration: none;
letter-spacing: 0.06em;
border-bottom: none;
transition: text-shadow 0.3s;
}
nav .site:hover {
text-shadow: 0 0 8px rgba(255,255,255,0.3);
}
nav .links { display: flex; gap: 1.2rem; }
nav .links a {
font-size: 0.82rem;
color: #606060;
text-decoration: none;
border-bottom: none;
transition: color 0.2s, text-shadow 0.3s;
}
nav .links a:hover {
color: #c8c8c8;
text-shadow: 0 0 6px rgba(255,255,255,0.15);
}
/* greeting */
#greeting {
padding: 1.5rem 0 0;
font-size: 0.85rem;
color: #484848;
font-style: italic;
opacity: 0;
animation: fadeIn 1.5s ease forwards 0.3s;
}
@keyframes fadeIn {
to { opacity: 1; }
}
/* content */
.content { padding: 1.8rem 0 3rem; }
/* headings */
h1 {
font-size: 1.4rem;
font-weight: 600;
color: #e8e8e8;
margin-bottom: 1rem;
}
h1 a { color: #e8e8e8; text-decoration: none; border-bottom: none; }
h1 a:hover { color: #999; }
h2 {
font-size: 1.05rem;
font-weight: 600;
color: #d0d0d0;
margin: 1.8rem 0 0.5rem;
}
p { margin: 0.5rem 0; color: #999; }
a {
color: #c8c8c8;
text-decoration: none;
border-bottom: 1px solid #2e2e2e;
transition: all 0.2s;
}
a:hover {
color: #fff;
border-bottom-color: #555;
}
em { color: #777; }
/* inputs */
input[type="text"],
input[type="url"],
input[name="q"],
input[name="url"],
input[name="note"],
input[name="tags"],
input[name="site_name"],
input[name="dest_hash"] {
background: #181818;
border: 1px solid #2a2a2a;
border-radius: 4px;
padding: 0.6rem 0.85rem;
color: #d0d0d0;
font-family: 'IBM Plex Sans', sans-serif;
font-size: 0.95rem;
transition: border-color 0.2s, box-shadow 0.3s;
}
input:focus, textarea:focus {
outline: none;
border-color: #454545;
box-shadow: 0 0 12px rgba(255,255,255,0.03);
}
button, input[type="submit"] {
background: #1c1c1c;
border: 1px solid #303030;
border-radius: 4px;
padding: 0.6rem 1.1rem;
color: #999;
font-family: 'IBM Plex Sans', sans-serif;
font-size: 0.88rem;
transition: all 0.2s;
}
button:hover, input[type="submit"]:hover {
background: #242424;
color: #d0d0d0;
border-color: #454545;
box-shadow: 0 0 10px rgba(255,255,255,0.04);
}
/* search results */
.result {
padding: 1rem 0;
border-bottom: 1px solid #1c1c1c;
transition: background 0.2s;
}
.result:hover {
background: rgba(255,255,255,0.01);
}
.result:last-child { border-bottom: none; }
.result > a:first-child {
font-size: 1.02rem;
font-weight: 500;
color: #ddd;
border-bottom: none;
}
.result > a:first-child:hover { color: #fff; }
.note {
margin-top: 0.3rem;
font-size: 0.9rem;
color: #606060;
}
.tags { margin-top: 0.3rem; }
.tag, .tags a {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.7rem;
color: #555;
border: 1px solid #252525;
border-radius: 3px;
padding: 0.1rem 0.35rem;
margin-right: 0.25rem;
}
.tag:hover, .tags a:hover {
color: #999;
border-color: #404040;
}
/* trusted / remote results */
details {
margin: 1rem 0;
border: 1px solid #1e1e1e;
border-radius: 4px;
padding: 0.7rem 0.9rem;
background: #151515;
}
summary {
font-size: 0.85rem;
color: #606060;
font-weight: 500;
}
summary:hover { color: #999; }
details ul { margin-top: 0.5rem; padding-left: 1.2rem; }
details li { margin: 0.35rem 0; font-size: 0.9rem; }
/* lists */
ul, ol { padding-left: 1.2rem; margin: 0.5rem 0; }
li { margin: 0.45rem 0; color: #999; }
li a { border-bottom: none; }
li a:hover { border-bottom: 1px solid #444; }
/* code */
pre {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.8rem;
background: #151515;
border: 1px solid #232323;
border-radius: 4px;
padding: 0.9rem;
overflow-x: auto;
color: #808080;
margin: 0.8rem 0;
}
code {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.82rem;
background: #1a1a1a;
border-radius: 3px;
padding: 0.1rem 0.35rem;
color: #999;
}
/* textarea */
textarea {
background: #151515;
border: 1px solid #2a2a2a;
border-radius: 4px;
padding: 0.7rem 0.9rem;
color: #c8c8c8;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.8rem;
line-height: 1.6;
resize: vertical;
width: 100%;
}
/* tables */
table { width: 100%; border-collapse: collapse; margin: 1rem 0; }
th {
text-align: left;
font-size: 0.72rem;
font-weight: 500;
color: #505050;
text-transform: uppercase;
letter-spacing: 0.08em;
padding: 0.5rem 0.7rem;
border-bottom: 1px solid #232323;
}
td {
padding: 0.5rem 0.7rem;
border-bottom: 1px solid #191919;
font-size: 0.9rem;
}
/* misc */
label { color: #999; }
input[type="checkbox"] { accent-color: #555; }
hr { border: none; border-top: 1px solid #1e1e1e; margin: 1rem 0; }
small {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.7rem;
color: #484848;
}
/* footer */
footer {
border-top: 1px solid #1c1c1c;
padding: 1.5rem 0 2rem;
text-align: center;
color: #333;
font-size: 0.8rem;
}
footer .clock {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.72rem;
color: #282828;
margin-top: 0.25rem;
}
::selection { background: #333; color: #fff; }
::-webkit-scrollbar { width: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #222; border-radius: 3px; }
@media (max-width: 600px) {
nav { flex-direction: column; gap: 0.5rem; }
nav .links { gap: 0.8rem; flex-wrap: wrap; }
h1 { font-size: 1.2rem; }
}
</style>
</head>
<body>
<canvas id="particles"></canvas>
<canvas id="trail"></canvas>
<canvas id="kodama"></canvas>
<div class="shell">
<nav>
<a class="site" href="/">tinyweb</a>
<div class="links">
<a href="/pages">browse</a>
<a href="/tags">tags</a>
<a href="/subscriptions">network</a>
<a href="/style">customize</a>
<a href="/about">about</a>
</div>
</nav>
<div id="greeting"></div>
<div class="content">
{{content}}
</div>
<footer>
<div>curated by hand · shared over mesh</div>
<div class="clock" id="clock"></div>
</footer>
</div>
<script>
(function() {
// greeting
var h = new Date().getHours();
var g = h < 5 ? "still up? the quiet hours are good for finding things." :
h < 12 ? "morning. what are you looking for?" :
h < 17 ? "afternoon. the index is ready." :
h < 21 ? "evening. settle in." :
"late night. good browsing ahead.";
document.getElementById('greeting').textContent = g;
// clock
function tick() {
var d = new Date();
var el = document.getElementById('clock');
if (el) el.textContent = d.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'});
}
tick();
setInterval(tick, 30000);
// floating dust particles
var pc = document.getElementById('particles');
var pctx = pc.getContext('2d');
var dots = [];
function resizeParticles() {
pc.width = window.innerWidth;
pc.height = window.innerHeight;
}
resizeParticles();
window.addEventListener('resize', resizeParticles);
for (var i = 0; i < 40; i++) {
dots.push({
x: Math.random() * pc.width,
y: Math.random() * pc.height,
vy: -(Math.random() * 0.15 + 0.05),
vx: (Math.random() - 0.5) * 0.1,
r: Math.random() * 1.2 + 0.3,
o: Math.random() * 0.25 + 0.05,
drift: Math.random() * Math.PI * 2
});
}
function drawParticles() {
pctx.clearRect(0, 0, pc.width, pc.height);
var t = Date.now() * 0.001;
for (var i = 0; i < dots.length; i++) {
var d = dots[i];
d.x += d.vx + Math.sin(t + d.drift) * 0.05;
d.y += d.vy;
if (d.y < -5) { d.y = pc.height + 5; d.x = Math.random() * pc.width; }
if (d.x < -5) d.x = pc.width + 5;
if (d.x > pc.width + 5) d.x = -5;
var flicker = d.o * (0.6 + 0.4 * Math.sin(t * 1.5 + d.drift));
pctx.beginPath();
pctx.arc(d.x, d.y, d.r, 0, Math.PI * 2);
pctx.fillStyle = 'rgba(200, 200, 200, ' + flicker + ')';
pctx.fill();
}
requestAnimationFrame(drawParticles);
}
drawParticles();
// cursor trail
var tc = document.getElementById('trail');
var tctx = tc.getContext('2d');
var points = [];
var mx = 0, my = 0;
function resizeTrail() {
tc.width = window.innerWidth;
tc.height = window.innerHeight;
}
resizeTrail();
window.addEventListener('resize', resizeTrail);
document.addEventListener('mousemove', function(e) {
mx = e.clientX;
my = e.clientY;
points.push({ x: mx, y: my, t: Date.now() });
if (points.length > 30) points.shift();
});
function drawTrail() {
tctx.clearRect(0, 0, tc.width, tc.height);
var now = Date.now();
// fade out points older than 400ms
while (points.length && now - points[0].t > 400) points.shift();
if (points.length > 1) {
for (var i = 1; i < points.length; i++) {
var age = (now - points[i].t) / 400;
var alpha = (1 - age) * 0.25;
var width = (1 - age) * 2;
tctx.beginPath();
tctx.moveTo(points[i-1].x, points[i-1].y);
tctx.lineTo(points[i].x, points[i].y);
tctx.strokeStyle = 'rgba(200, 200, 200, ' + alpha + ')';
tctx.lineWidth = width;
tctx.lineCap = 'round';
tctx.stroke();
}
}
requestAnimationFrame(drawTrail);
}
drawTrail();
// kodama (tree spirits)
var kc = document.getElementById('kodama');
var kctx = kc.getContext('2d');
var spirits = [];
var numSpirits = 8;
function resizeKodama() {
kc.width = window.innerWidth;
kc.height = window.innerHeight;
}
resizeKodama();
window.addEventListener('resize', resizeKodama);
for (var i = 0; i < numSpirits; i++) {
// each spirit gets unique proportions
var headR = 0.38 + Math.random() * 0.12; // head radius ratio (bigger = bigger head)
var bodyH = 0.25 + Math.random() * 0.2; // body height ratio
var bodyW = 0.3 + Math.random() * 0.15; // body width ratio
var eyeSpread = 0.18 + Math.random() * 0.1; // how far apart eyes are
var eyeSize = 0.055 + Math.random() * 0.03; // eye dot size
var eyeY = -0.08 + Math.random() * 0.08; // eye vertical position
var hasMouth = Math.random() > 0.3; // 70% have visible mouth
var mouthSize = 0.03 + Math.random() * 0.03;
var mouthY = 0.15 + Math.random() * 0.1;
var hasArms = Math.random() > 0.4; // 60% have little arm bumps
var glowSize = 1.4 + Math.random() * 0.8; // glow radius multiplier
var glowAlpha = 0.08 + Math.random() * 0.12; // glow brightness
var tint = Math.floor(Math.random() * 15); // slight warm/cool variation
spirits.push({
x: Math.random() * 0.8 + 0.1,
baseY: 0.75 + Math.random() * 0.18,
size: 10 + Math.random() * 12,
phase: Math.random() * Math.PI * 2,
tiltSpeed: 1.2 + Math.random() * 2,
bobSpeed: 0.6 + Math.random() * 0.8,
opacity: 0,
targetOpacity: 0.4 + Math.random() * 0.45,
fadeSpeed: 0.002 + Math.random() * 0.004,
appearing: true,
timer: Math.random() * 600,
lifespan: 500 + Math.random() * 600,
rattleTime: 0,
rattling: false,
// unique shape params
headR: headR,
bodyH: bodyH,
bodyW: bodyW,
eyeSpread: eyeSpread,
eyeSize: eyeSize,
eyeY: eyeY,
hasMouth: hasMouth,
mouthSize: mouthSize,
mouthY: mouthY,
hasArms: hasArms,
glowSize: glowSize,
glowAlpha: glowAlpha,
tint: tint,
// 8 offsets that warp the head into a unique rock-like blob
hw: [
(Math.random()-0.5)*0.25, (Math.random()-0.5)*0.2,
(Math.random()-0.5)*0.2, (Math.random()-0.5)*0.25,
(Math.random()-0.5)*0.25, (Math.random()-0.5)*0.2,
(Math.random()-0.5)*0.2, (Math.random()-0.5)*0.25
],
headTall: 0.8 + Math.random() * 0.5 // overall tall vs wide
});
}
function drawKodamaSpirit(x, y, size, tilt, opacity, sp) {
kctx.save();
kctx.translate(x, y);
kctx.globalAlpha = opacity;
var r = size * sp.headR; // head radius
// outer glow aura
var grd = kctx.createRadialGradient(0, -size * 0.1, r * 0.3, 0, -size * 0.1, r * sp.glowSize);
grd.addColorStop(0, 'rgba(255, 255, 250, ' + sp.glowAlpha + ')');
grd.addColorStop(0.5, 'rgba(255, 255, 250, ' + (sp.glowAlpha * 0.3) + ')');
grd.addColorStop(1, 'rgba(255, 255, 250, 0)');
kctx.fillStyle = grd;
kctx.beginPath();
kctx.arc(0, -size * 0.1, r * sp.glowSize, 0, Math.PI * 2);
kctx.fill();
// body - stubby rounded shape
var bw = size * sp.bodyW;
var bh = size * sp.bodyH;
var by = size * 0.15;
kctx.fillStyle = 'rgb(' + (238 + sp.tint) + ',' + (237 + sp.tint) + ',' + (230 + sp.tint) + ')';
kctx.beginPath();
kctx.ellipse(0, by + bh * 0.4, bw * 0.5, bh * 0.55, 0, 0, Math.PI * 2);
kctx.fill();
// arms - tiny bumps on sides
if (sp.hasArms) {
kctx.beginPath();
kctx.ellipse(-bw * 0.5 - size * 0.04, by + bh * 0.1, size * 0.05, size * 0.04, -0.3, 0, Math.PI * 2);
kctx.fill();
kctx.beginPath();
kctx.ellipse(bw * 0.5 + size * 0.04, by + bh * 0.1, size * 0.05, size * 0.04, 0.3, 0, Math.PI * 2);
kctx.fill();
}
// head (tilts)
kctx.save();
kctx.rotate(tilt);
// head - unique rock-like blob shape per spirit
var hx = 0, hy = -size * 0.15;
var rx = r, ry = r * sp.headTall;
var w = sp.hw;
kctx.fillStyle = 'rgb(' + (243 + sp.tint) + ',' + (242 + sp.tint) + ',' + (237 + sp.tint) + ')';
kctx.beginPath();
// top
kctx.moveTo(hx + r * w[0], hy - ry);
// top-right
kctx.bezierCurveTo(
hx + rx * (0.55 + w[0]), hy - ry * (0.9 + w[1]),
hx + rx * (1.0 + w[1]), hy - ry * (0.4 + w[0]),
hx + rx * (1.0 + w[2]), hy + ry * w[2]);
// bottom-right
kctx.bezierCurveTo(
hx + rx * (1.0 + w[3]), hy + ry * (0.5 + w[2]),
hx + rx * (0.5 + w[3]), hy + ry * (1.0 + w[3]),
hx + r * w[4], hy + ry * (0.95 + w[4] * 0.3));
// bottom-left
kctx.bezierCurveTo(
hx - rx * (0.5 + w[5]), hy + ry * (1.0 + w[5]),
hx - rx * (1.0 + w[5]), hy + ry * (0.5 + w[4]),
hx - rx * (1.0 + w[6]), hy + ry * w[6]);
// top-left
kctx.bezierCurveTo(
hx - rx * (1.0 + w[7]), hy - ry * (0.4 + w[6]),
hx - rx * (0.55 + w[7]),hy - ry * (0.9 + w[7]),
hx + r * w[0], hy - ry);
kctx.fill();
// subtle inner highlight
kctx.fillStyle = 'rgba(255, 255, 252, 0.25)';
kctx.beginPath();
kctx.arc(-rx * 0.12, hy - ry * 0.1, r * 0.45, 0, Math.PI * 2);
kctx.fill();
// eyes - small round dark dots
kctx.fillStyle = 'rgba(15, 15, 15, 0.9)';
var ey = -size * 0.15 + size * sp.eyeY;
var es = size * sp.eyeSize;
kctx.beginPath();
kctx.arc(-size * sp.eyeSpread, ey, es, 0, Math.PI * 2);
kctx.fill();
kctx.beginPath();
kctx.arc(size * sp.eyeSpread, ey, es, 0, Math.PI * 2);
kctx.fill();
// mouth - tiny dot, not all have one
if (sp.hasMouth) {
kctx.fillStyle = 'rgba(15, 15, 15, 0.7)';
kctx.beginPath();
kctx.arc(0, ey + size * sp.mouthY, size * sp.mouthSize, 0, Math.PI * 2);
kctx.fill();
}
kctx.restore(); // head tilt
kctx.restore(); // position
}
function drawKodama() {
kctx.clearRect(0, 0, kc.width, kc.height);
var t = Date.now() * 0.001;
for (var i = 0; i < spirits.length; i++) {
var s = spirits[i];
s.timer++;
if (s.appearing) {
s.opacity += s.fadeSpeed;
if (s.opacity >= s.targetOpacity) s.opacity = s.targetOpacity;
if (s.timer > s.lifespan) s.appearing = false;
} else {
s.opacity -= s.fadeSpeed;
if (s.opacity <= 0) {
s.opacity = 0;
s.x = Math.random() * 0.8 + 0.1;
s.baseY = 0.75 + Math.random() * 0.18;
s.targetOpacity = 0.4 + Math.random() * 0.45;
s.timer = 0;
s.lifespan = 500 + Math.random() * 600;
s.appearing = true;
s.rattleTime = 0;
}
}
if (s.opacity <= 0) continue;
if (!s.rattling && Math.random() < 0.004) {
s.rattling = true;
s.rattleTime = 0;
}
var tilt = Math.sin(t * s.tiltSpeed + s.phase) * 0.1;
if (s.rattling) {
s.rattleTime++;
tilt = Math.sin(s.rattleTime * 0.9) * 0.35 * Math.max(0, 1 - s.rattleTime / 25);
if (s.rattleTime > 25) s.rattling = false;
}
var bobY = Math.sin(t * s.bobSpeed + s.phase) * 2.5;
var px = s.x * kc.width;
var py = s.baseY * kc.height + bobY;
drawKodamaSpirit(px, py, s.size, tilt, s.opacity, s);
}
requestAnimationFrame(drawKodama);
}
drawKodama();
})();
</script>
</body>
</html>